Compare commits

..

4 Commits

Author SHA1 Message Date
Andrew Shumway
0d94e45da3 poetry lock 2024-05-21 11:41:27 -06:00
Andrew Shumway
3a6e5e9c2d Pull main 2024-05-20 10:25:43 -06:00
Andrew Shumway
ed827d0c29 Fix tests 2024-05-20 10:24:50 -06:00
Andrew Shumway
013154d5cf Refactor create/start job 2024-05-14 09:11:19 -06:00
115 changed files with 1948 additions and 3324 deletions

View File

@@ -1,706 +0,0 @@
{
"version": "1.5.0",
"plugins_used": [
{
"name": "ArtifactoryDetector"
},
{
"name": "AWSKeyDetector"
},
{
"name": "AzureStorageKeyDetector"
},
{
"name": "Base64HighEntropyString",
"limit": 4.5
},
{
"name": "BasicAuthDetector"
},
{
"name": "CloudantDetector"
},
{
"name": "DiscordBotTokenDetector"
},
{
"name": "GitHubTokenDetector"
},
{
"name": "GitLabTokenDetector"
},
{
"name": "HexHighEntropyString",
"limit": 3.0
},
{
"name": "IbmCloudIamDetector"
},
{
"name": "IbmCosHmacDetector"
},
{
"name": "IPPublicDetector"
},
{
"name": "JwtTokenDetector"
},
{
"name": "KeywordDetector",
"keyword_exclude": ""
},
{
"name": "MailchimpDetector"
},
{
"name": "NpmDetector"
},
{
"name": "OpenAIDetector"
},
{
"name": "PrivateKeyDetector"
},
{
"name": "PypiTokenDetector"
},
{
"name": "SendGridDetector"
},
{
"name": "SlackDetector"
},
{
"name": "SoftlayerDetector"
},
{
"name": "SquareOAuthDetector"
},
{
"name": "StripeDetector"
},
{
"name": "TelegramBotTokenDetector"
},
{
"name": "TwilioKeyDetector"
}
],
"filters_used": [
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
},
{
"path": "detect_secrets.filters.common.is_baseline_file",
"filename": ".ds.baseline"
},
{
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
},
{
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
},
{
"path": "detect_secrets.filters.heuristic.is_likely_id_string"
},
{
"path": "detect_secrets.filters.heuristic.is_lock_file"
},
{
"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
},
{
"path": "detect_secrets.filters.heuristic.is_potential_uuid"
},
{
"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
},
{
"path": "detect_secrets.filters.heuristic.is_sequential_string"
},
{
"path": "detect_secrets.filters.heuristic.is_swagger_file"
},
{
"path": "detect_secrets.filters.heuristic.is_templated_secret"
}
],
"results": {
".github/workflows/checks.yml": [
{
"type": "Secret Keyword",
"filename": ".github/workflows/checks.yml",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 61,
"is_secret": false
},
{
"type": "Basic Auth Credentials",
"filename": ".github/workflows/checks.yml",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 95,
"is_secret": false
}
],
"app/assets/js/uswds.min.js": [
{
"type": "Secret Keyword",
"filename": "app/assets/js/uswds.min.js",
"hashed_secret": "372ea08cab33e71c02c651dbc83a474d32c676ea",
"is_verified": false,
"line_number": 85,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "app/assets/js/uswds.min.js",
"hashed_secret": "53e07a32bf191d6917ee6fd863f0b52632a86798",
"is_verified": false,
"line_number": 85,
"is_secret": false
}
],
"app/config.py": [
{
"type": "Secret Keyword",
"filename": "app/config.py",
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
"is_verified": false,
"line_number": 117,
"is_secret": false
}
],
"app/main/_commonly_used_passwords.py": [
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "82e19fa12aab7cfc718a002fc82c0f074bf070e7",
"is_verified": false,
"line_number": 123,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "a172ffc990129fe6f68b50f6037c54a1894ee3fd",
"is_verified": false,
"line_number": 240,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "4de69ee6b12b7fc91070873b71ba6e2929b90619",
"is_verified": false,
"line_number": 244,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "370194ff6e0f93a7432e16cc9badd9427e8b4e13",
"is_verified": false,
"line_number": 284,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "3dd635a808ddb6dd4b6731f7c409d53dd4b14df2",
"is_verified": false,
"line_number": 356,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "67a74306b06d0c01624fe0d0249a570f4d093747",
"is_verified": false,
"line_number": 374,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "61d6504733ca7757e259c644acd085c4dd471019",
"is_verified": false,
"line_number": 910,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "4ea872dfd7eefbde0036da7f0780826353dc7477",
"is_verified": false,
"line_number": 940,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "b214f706bb602c1cc2adc5c6165e73622305f4bb",
"is_verified": false,
"line_number": 1010,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "5cbabd43e49a1fedbbc3b86311aa6c8fe446abf9",
"is_verified": false,
"line_number": 1195,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "18ad10fd4a67f21fc07b1aa5046b410f6b2bedf1",
"is_verified": false,
"line_number": 1213,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "10470c3b4b1fed12c3baac014be15fac67c6e815",
"is_verified": false,
"line_number": 1263,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "65e1946c8f102eca8ba0af291f7c5e807516d94c",
"is_verified": false,
"line_number": 1346,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "0075df0a74c07ee295c98238c018401c9a80183b",
"is_verified": false,
"line_number": 1397,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "ca0023d7b345802fbc227b902cb9c57a3e02195f",
"is_verified": false,
"line_number": 1442,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "c8c6ca2e11c2dfd2a40914585b5944bffea15c8c",
"is_verified": false,
"line_number": 1555,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "b85b97a99eab8c809570c61d6404c1e49bdefbb4",
"is_verified": false,
"line_number": 1596,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "dec7dd342a499dfd4d283d872ccf598d8a7b6039",
"is_verified": false,
"line_number": 1789,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "2dc5053699a351121bf839c446bd4a878dda5735",
"is_verified": false,
"line_number": 1939,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "e5d54f0ac13abbdaa94b696c2469148b96dd11ab",
"is_verified": false,
"line_number": 2242,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "6059f42e2bbae78141e8a9e6286755ee691d5ce0",
"is_verified": false,
"line_number": 2305,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "fe703d258c7ef5f50b71e06565a65aa07194907f",
"is_verified": false,
"line_number": 2348,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "c229b68e1c3ffd9874838b5cb5354a0ee1367ddc",
"is_verified": false,
"line_number": 2349,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "756de479126e911b6f3400ae686d663d9d26b509",
"is_verified": false,
"line_number": 2920,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "6b174322afcdb440ee9cc3cc11eb16f9a00dec04",
"is_verified": false,
"line_number": 2975,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "9860783bfb510cbb2bf34471ec0b84a7ea587695",
"is_verified": false,
"line_number": 3359,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "b227cbd22eaa96019ebfc4aff35ad2add2a47439",
"is_verified": false,
"line_number": 3590,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "381d48209aecab8834eb495c5b5406100da07882",
"is_verified": false,
"line_number": 3811,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "508b38590a90d32990aadd7350d160b795c3ab41",
"is_verified": false,
"line_number": 3850,
"is_secret": false
}
],
"app/main/views/sign_in.py": [
{
"type": "Private Key",
"filename": "app/main/views/sign_in.py",
"hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9",
"is_verified": false,
"line_number": 27,
"is_secret": false
}
],
"app/templates/new/components/head.html": [
{
"type": "Base64 High Entropy String",
"filename": "app/templates/new/components/head.html",
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
"is_verified": false,
"line_number": 34,
"is_secret": false
}
],
"app/templates/old/admin_template.html": [
{
"type": "Base64 High Entropy String",
"filename": "app/templates/old/admin_template.html",
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
"is_verified": false,
"line_number": 18,
"is_secret": false
}
],
"deploy-config/sandbox.yml": [
{
"type": "Secret Keyword",
"filename": "deploy-config/sandbox.yml",
"hashed_secret": "113151dd10316fcb0d5507b6215d78e2f3fe9e54",
"is_verified": false,
"line_number": 8,
"is_secret": false
}
],
"pytest.ini": [
{
"type": "Secret Keyword",
"filename": "pytest.ini",
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
"is_verified": false,
"line_number": 7,
"is_secret": false
},
{
"type": "Base64 High Entropy String",
"filename": "pytest.ini",
"hashed_secret": "d347784b1ab6074a65cda7bc42f1561bed85493f",
"is_verified": false,
"line_number": 7,
"is_secret": false
},
{
"type": "Base64 High Entropy String",
"filename": "pytest.ini",
"hashed_secret": "ed1754d5cc82c8fd83205ebfb8c43fe4e88415a4",
"is_verified": false,
"line_number": 9,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "pytest.ini",
"hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
"is_verified": false,
"line_number": 11,
"is_secret": false
}
],
"tests/__init__.py": [
{
"type": "Secret Keyword",
"filename": "tests/__init__.py",
"hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea",
"is_verified": false,
"line_number": 388,
"is_secret": false
}
],
"tests/app/main/forms/test_register_user_form.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/forms/test_register_user_form.py",
"hashed_secret": "8c6c978dc8e08771c7dea1ea2370fdf2446e5ba5",
"is_verified": false,
"line_number": 38,
"is_secret": false
}
],
"tests/app/main/test_errorhandlers.py": [
{
"type": "Base64 High Entropy String",
"filename": "tests/app/main/test_errorhandlers.py",
"hashed_secret": "005fa73b3f2be8f0d71d361c1f0a9d787cd09b4e",
"is_verified": false,
"line_number": 33,
"is_secret": false
}
],
"tests/app/main/test_request_header.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/test_request_header.py",
"hashed_secret": "6866ef97a972ba3a2c6ff8bb2812981054770162",
"is_verified": false,
"line_number": 21,
"is_secret": false
}
],
"tests/app/main/views/organizations/test_organization_invites.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/organizations/test_organization_invites.py",
"hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473",
"is_verified": false,
"line_number": 265,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/organizations/test_organization_invites.py",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 273,
"is_secret": false
}
],
"tests/app/main/views/test_accept_invite.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_accept_invite.py",
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
"is_verified": false,
"line_number": 607,
"is_secret": false
}
],
"tests/app/main/views/test_new_password.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_new_password.py",
"hashed_secret": "a41d5c3bbcd0b39c627b9cbf4897c6d25efa694f",
"is_verified": false,
"line_number": 89,
"is_secret": false
}
],
"tests/app/main/views/test_register.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473",
"is_verified": false,
"line_number": 116,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 192,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "bb5b7caa27d005d38039e3797c3ddb9bcd22c3c8",
"is_verified": false,
"line_number": 260,
"is_secret": false
}
],
"tests/app/main/views/test_sign_in.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_sign_in.py",
"hashed_secret": "8b8b69116ee882b5e987e330f55db81aba0636f9",
"is_verified": false,
"line_number": 90,
"is_secret": false
}
],
"tests/app/main/views/test_two_factor.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_two_factor.py",
"hashed_secret": "dc66ad927c29e31c6c374231f57a4684b0687bfe",
"is_verified": false,
"line_number": 267,
"is_secret": false
}
],
"tests/app/main/views/test_user_profile.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_user_profile.py",
"hashed_secret": "8072d7aad32964ec43fbcb699c75dc38890792f7",
"is_verified": false,
"line_number": 350,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_user_profile.py",
"hashed_secret": "4c9dbb972da179e4f66f023eaa5fb9451d835030",
"is_verified": false,
"line_number": 351,
"is_secret": false
}
],
"tests/app/main/views/test_verify.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_verify.py",
"hashed_secret": "faafcfa63e128929409bf310b7ea5a415f2331ce",
"is_verified": false,
"line_number": 160,
"is_secret": false
}
],
"tests/app/notify_client/test_user_client.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/notify_client/test_user_client.py",
"hashed_secret": "f2c57870308dc87f432e5912d4de6f8e322721ba",
"is_verified": false,
"line_number": 55,
"is_secret": false
}
],
"tests/app/test_cloudfoundry_config.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/test_cloudfoundry_config.py",
"hashed_secret": "5e44dae2de8b6e57c797b968035265c9f2cd2b3e",
"is_verified": false,
"line_number": 12,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/test_cloudfoundry_config.py",
"hashed_secret": "e5e178db7317356946d13e5d2da037d39ac61c71",
"is_verified": false,
"line_number": 27,
"is_secret": false
}
],
"tests/conftest.py": [
{
"type": "Secret Keyword",
"filename": "tests/conftest.py",
"hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea",
"is_verified": false,
"line_number": 3266,
"is_secret": false
}
],
"tests/notifications_utils/clients/antivirus/test_antivirus_client.py": [
{
"type": "Secret Keyword",
"filename": "tests/notifications_utils/clients/antivirus/test_antivirus_client.py",
"hashed_secret": "932b25270abe1301c22c709a19082dff07d469ff",
"is_verified": false,
"line_number": 16,
"is_secret": false
}
],
"tests/notifications_utils/clients/encryption/test_encryption_client.py": [
{
"type": "Secret Keyword",
"filename": "tests/notifications_utils/clients/encryption/test_encryption_client.py",
"hashed_secret": "f1e923a9667de11be6a210849a8651c1bfd81605",
"is_verified": false,
"line_number": 13,
"is_secret": false
}
],
"tests/notifications_utils/clients/zendesk/test_zendesk_client.py": [
{
"type": "Secret Keyword",
"filename": "tests/notifications_utils/clients/zendesk/test_zendesk_client.py",
"hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6",
"is_verified": false,
"line_number": 16,
"is_secret": false
}
]
},
"generated_at": "2024-06-11T22:26:18Z"
}

View File

@@ -9,10 +9,10 @@ runs:
sudo apt-get update \
&& sudo apt-get install -y --no-install-recommends \
libcurl4-openssl-dev
- name: Set up Python 3.12.3
- name: Set up Python 3.12
uses: actions/setup-python@v4
with:
python-version: "3.12.3"
python-version: "3.12"
- name: Install poetry
shell: bash
run: pip install poetry

View File

@@ -1,95 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main", "production" ]
pull_request:
branches: [ "main", "production" ]
schedule:
- cron: '18 5 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
- language: python
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -18,7 +18,7 @@ jobs:
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v41.0.0
with:
files: |
terraform/demo
@@ -88,7 +88,7 @@ jobs:
- name: Check for changes to egress config
id: changed-egress-config
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v41.0.0
with:
files: |
deploy-config/egress_proxy/notify-admin-demo.*.acl

View File

@@ -18,7 +18,7 @@ jobs:
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v41.0.0
with:
files: |
terraform/production
@@ -88,7 +88,7 @@ jobs:
- name: Check for changes to egress config
id: changed-egress-config
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v41.0.0
with:
files: |
deploy-config/egress_proxy/notify-admin-production.*.acl

View File

@@ -23,7 +23,7 @@ jobs:
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v41.0.0
with:
files: |
terraform/staging
@@ -95,7 +95,7 @@ jobs:
- name: Check for changes to egress config
id: changed-egress-config
uses: tj-actions/changed-files@v44
uses: tj-actions/changed-files@v41.0.0
with:
files: |
deploy-config/egress_proxy/notify-admin-staging.*.acl

2
.nvmrc
View File

@@ -1 +1 @@
16.20.2
16.15.1

View File

@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -11,14 +11,3 @@ repos:
- id: debug-statements
- id: check-merge-conflict
- id: check-toml
- id: check-ast
- id: fix-byte-order-marker
- id: detect-aws-credentials
args: [--allow-missing-credentials]
- id: detect-private-key
- id: mixed-line-ending
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.ds.baseline']

View File

@@ -21,8 +21,7 @@ bootstrap: generate-version-file ## Set up everything to run the app
poetry install --sync --no-root
poetry run playwright install --with-deps
poetry run pre-commit install
source $(NVMSH) --no-use && nvm install && npm install
source $(NVMSH) && npm ci --no-audit
source $(NVMSH) --no-use && nvm install && npm ci --no-audit
source $(NVMSH) && npm run build
.PHONY: watch-frontend
@@ -41,10 +40,6 @@ run-flask-bare: ## Run flask without invoking poetry so we can override ENV var
npm-audit: ## Check for vulnerabilities in NPM packages
source $(NVMSH) && npm run audit
.PHONY: npm-audit-fix
npm-audit-fix: ## Fix vulnerabilities that do not require attentino (according to npm)
source $(NVMSH) && npm audit fix
.PHONY: help
help:
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@@ -81,9 +76,8 @@ py-test: ## Run python unit tests
poetry run coverage html -d .coverage_cache
.PHONY: dead-code
dead-code: ## 60% is our aspirational goal, but currently breaks the build
poetry run vulture ./app ./notifications_utils --min-confidence=100
dead-code:
poetry run vulture ./app --min-confidence=100
.PHONY: e2e-test
e2e-test: export NEW_RELIC_ENVIRONMENT=test

View File

@@ -40,7 +40,7 @@ You will need the following items:
[Follow the instructions here to set up the Notify.gov API.](https://github.com/GSA/notifications-api#before-you-start)
The Notify.gov API is required in order for the Notify.gov Admin UI to run, and
it will also take care of many of the steps that are listed here. The sections
it will also take care of many of the steps that are listed here. The sections
that are a repeat from the API setup are flagged with an **[API Step]** label
in front of them.
@@ -83,13 +83,11 @@ Your system `$PATH` environment variable is likely set in one of these
locations:
For BASH shells:
- `~/.bashrc`
- `~/.bash_profile`
- `~/.profile`
For ZSH shells:
- `~/.zshrc`
- `~/.zprofile`
@@ -99,7 +97,7 @@ environments.
Which file you need to modify depends on whether or not you are running an
interactive shell or a login shell
(see [this Stack Overflow post](https://stackoverflow.com/questions/18186929/what-are-the-differences-between-a-login-shell-and-interactive-shell)
for an explanation of the differences). If you're still not sure, please ask
for an explanation of the differences). If you're still not sure, please ask
the team for help!
Once you determine which file you'll need to modify, add these lines before any
@@ -160,7 +158,7 @@ _NOTE: This project currently uses the latest `1.4.x release of Terraform._
#### [API Step] Python Installation
Now we're going to install a tool to help us manage Python versions and
virtual environments on our system. First, we'll install
virtual environments on our system. First, we'll install
[pyenv](https://github.com/pyenv/pyenv) and one of its plugins,
[pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv), with Homebrew:
@@ -287,7 +285,7 @@ we'll use `3.12` in our example here since we recently upgraded to this version:
pyenv install 3.12
```
Next, delete the virtual environment you previously had set up. If you followed
Next, delete the virtual environment you previously had set up. If you followed
the instructions above with the first-time set up, you can do this with `pyenv`:
```sh
@@ -308,20 +306,6 @@ you'll be set with an upgraded version of Python.
_If you're not sure about the details of your current virtual environment, you can run `poetry env info` to get more information. If you've been using `pyenv` for everything, you can also see all available virtual environments with `pyenv virtualenvs`._
#### Updating the .env file for Login.gov
To configure the application for Login.gov, you will need to update the following environment variables in the .env file:
```
COMMIT_HASH=”--------”
```
Reach out to someone on the team to get the most recent Login.gov key.
```
LOGIN_PEM="INSERT_LOGIN_GOV_KEY_HERE"
```
#### Updating the .env file for E2E tests
With the newly created `.env` file in place, you'll need to make one more
@@ -369,7 +353,7 @@ API is running as well!
## Creating a 'First User' in the database
After you have completed all setup steps, you will be unable to log in, because there
will not be a user in the database to link to the login.gov account you are using. So
will not be a user in the database to link to the login.gov account you are using. So
you will need to create that user in your database using the 'create-test-user' command.
Open two terminals pointing to the api project and then run these commands in the
@@ -388,6 +372,8 @@ is the same one you are using in login.gov and make sure your phone number is in
If for any reason in the course of development it is necessary for your to delete your db
via the `dropdb` command, you will need to repeat these steps when you recreate your db.
## Git Hooks
We're using [`pre-commit`](https://pre-commit.com/) to manage hooks in order to

View File

@@ -18,7 +18,6 @@ from flask import (
)
from flask.globals import request_ctx
from flask_login import LoginManager, current_user
from flask_socketio import SocketIO
from flask_talisman import Talisman
from flask_wtf import CSRFProtect
from flask_wtf.csrf import CSRFError
@@ -31,7 +30,7 @@ from werkzeug.local import LocalProxy
from app import proxy_fix
from app.asset_fingerprinter import asset_fingerprinter
from app.config import configs
from app.extensions import redis_client
from app.extensions import redis_client, zendesk_client
from app.formatters import (
convert_markdown_template,
convert_to_boolean,
@@ -119,7 +118,6 @@ from notifications_utils.recipients import format_phone_number_human_readable
login_manager = LoginManager()
csrf = CSRFProtect()
talisman = Talisman()
socketio = SocketIO()
# The current service attached to the request stack.
@@ -177,7 +175,6 @@ def create_app(application):
init_govuk_frontend(application)
init_jinja(application)
socketio.init_app(application)
for client in (
csrf,
@@ -205,6 +202,7 @@ def create_app(application):
user_api_client,
# External API clients
redis_client,
zendesk_client,
):
client.init_app(application)
@@ -233,24 +231,6 @@ def create_app(application):
)
logging.init_app(application)
# Hopefully will help identify if there is a race condition causing the CSRF errors
# that we have occasionally seen in our environments.
for key in ("SECRET_KEY", "DANGEROUS_SALT"):
try:
value = application.config[key]
except KeyError:
application.logger.error(f"Env Var {key} doesn't exist.")
else:
try:
data_len = len(value.strip())
except (TypeError, AttributeError):
application.logger.error(f"Env Var {key} invalid type: {type(value)}")
else:
if data_len:
application.logger.info(f"Env Var {key} is a non-zero length.")
else:
application.logger.error(f"Env Var {key} is empty.")
login_manager.login_view = "main.sign_in"
login_manager.login_message_category = "default"
login_manager.session_protection = None

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 292.1 39.47"><path fill="#fff" d="M7.44 6.89h17.2v22.06H7.44z"/><path fill="#112e51" d="M53.45 5.28h-6.16v27.31h17.87v-5.38H53.45V5.28zm30.76-.19c-8.35 0-14.67 5.89-14.67 13.85s6.32 13.93 14.67 13.93 14.67-6 14.67-13.93S92.56 5.09 84.21 5.09zm.08 22.39a8.49 8.49 0 0 1-8.43-8.54 8.4 8.4 0 0 1 8.43-8.47 8.29 8.29 0 0 1 8.27 8.47 8.36 8.36 0 0 1-8.27 8.54zm35.36-17.05a11.43 11.43 0 0 1 7.49 3l3.47-4.41a17.21 17.21 0 0 0-11-4c-8.51 0-14.9 6-14.9 13.85s6.29 13.99 14.58 13.99a20.21 20.21 0 0 0 11.08-3.55V17.57h-10.11v4.82H125V26a12.42 12.42 0 0 1-5.34 1.48 8.52 8.52 0 1 1 0-17zm19.61-5.15h6.17v27.31h-6.17zm34.53 17.56L160.52 5.28h-5.61v27.31h5.89V15.07l13.22 17.52h5.62V5.28h-5.85v17.56zm17.47 2.85a3.39 3.39 0 0 0-3.47 3.56 3.47 3.47 0 1 0 6.94 0 3.36 3.36 0 0 0-3.47-3.56zm23.15-15.26a11.43 11.43 0 0 1 7.49 3l3.47-4.41a17.21 17.21 0 0 0-11-4c-8.51 0-14.9 6-14.9 13.85s6.28 13.93 14.55 13.93a20.21 20.21 0 0 0 11.08-3.55V17.57H215v4.82h4.77V26a12.42 12.42 0 0 1-5.34 1.48 8.52 8.52 0 1 1 0-17zm32.23-5.34C238.29 5.09 232 11 232 18.94s6.32 13.93 14.67 13.93 14.67-6 14.67-13.93S255 5.09 246.64 5.09zm.08 22.39a8.49 8.49 0 0 1-8.43-8.54 8.4 8.4 0 0 1 8.43-8.47 8.29 8.29 0 0 1 8.28 8.47 8.36 8.36 0 0 1-8.28 8.54zm39.02-22.2-7.49 20.6-7.45-20.6h-6.63l10.73 27.31h6.36L292.1 5.28h-6.36z"/><path fill="#e21d3e" d="m11.38 25.34 1.45-8.28a4.39 4.39 0 0 1 2.71-7.86V0H4.34A4.34 4.34 0 0 0 0 4.34v21.28a2.62 2.62 0 0 0 .26 1.16c.94 1.9 4.65 8 15.27 12.69V25.7a24.3 24.3 0 0 1-4.15-.36z"/><path fill="#b51e23" d="M26.72 0H15.53v9.19A4.39 4.39 0 0 1 18.28 17l1.45 8.32a24.29 24.29 0 0 1-4.2.36v13.79C26.14 34.8 29.86 28.68 30.8 26.79a2.62 2.62 0 0 0 .26-1.16V4.34A4.34 4.34 0 0 0 26.72 0z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,24 @@
(function (window) {
const ctx = document.getElementById('myChart');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
})(window);

View File

@@ -1,66 +0,0 @@
(function (window) {
function initializeChartAndSocket() {
var ctx = document.getElementById('myChart');
if (!ctx) {
return;
}
var myBarChart = new Chart(ctx.getContext('2d'), {
type: 'bar',
data: {
labels: [],
datasets: [
{
label: 'Delivered',
data: [],
backgroundColor: '#0076d6',
stack: 'Stack 0'
},
]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
var socket = io();
var serviceId = ctx.getAttribute('data-service-id');
socket.on('connect', function() {
socket.emit('fetch_daily_stats', serviceId);
});
socket.on('daily_stats_update', function(data) {
var labels = [];
var deliveredData = [];
for (var date in data) {
labels.push(date);
deliveredData.push(data[date].sms.delivered);
}
myBarChart.data.labels = labels;
myBarChart.data.datasets[0].data = deliveredData;
myBarChart.update();
});
socket.on('error', function(data) {
console.log('Error:', data);
});
var sevenDaysButton = document.getElementById('sevenDaysButton');
if (sevenDaysButton) {
sevenDaysButton.addEventListener('click', function() {
socket.emit('fetch_daily_stats', serviceId);
});
}
}
document.addEventListener('DOMContentLoaded', initializeChartAndSocket);
})(window);

View File

@@ -157,18 +157,6 @@ td.table-empty-message {
}
}
.usa-button img {
margin-left: .5rem;
height: 1rem;
}
.usa-button.login-button.login-button--primary,.login-button.login-button--primary:hover{
color:#112e51;background-color:#fff;
border:1px solid #767676;
display: inline-flex;
justify-content: center;
}
.user-list-edit-link:active:before,
.user-list-edit-link:focus:before {
box-shadow: none;

View File

@@ -38,7 +38,7 @@ class Config(object):
NR_MONITOR_ON = settings and settings.monitor_mode
COMMIT_HASH = getenv("COMMIT_HASH", "--------")[0:7]
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov", "mil", "si.edu"]
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov"]
# Logging
NOTIFY_LOG_LEVEL = getenv("NOTIFY_LOG_LEVEL", "INFO")
@@ -53,13 +53,7 @@ class Config(object):
PERMANENT_SESSION_LIFETIME = 1800 # 30 Minutes
SEND_FILE_MAX_AGE_DEFAULT = 365 * 24 * 60 * 60 # 1 year
REPLY_TO_EMAIL_ADDRESS_VALIDATION_TIMEOUT = 45
ACTIVITY_STATS_LIMIT_DAYS = {
"today": 0,
"one_day": 1,
"three_day": 3,
"five_day": 5,
"seven_day": 7,
}
ACTIVITY_STATS_LIMIT_DAYS = 7
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_NAME = "notify_admin_session"
SESSION_COOKIE_SECURE = True

View File

@@ -1,3 +1,5 @@
from notifications_utils.clients.redis.redis_client import RedisClient
from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient
zendesk_client = ZendeskClient()
redis_client = RedisClient()

View File

@@ -6,7 +6,6 @@ from itertools import groupby
from flask import Response, abort, jsonify, render_template, request, session, url_for
from flask_login import current_user
from flask_socketio import emit
from werkzeug.utils import redirect
from app import (
@@ -15,7 +14,6 @@ from app import (
job_api_client,
notification_api_client,
service_api_client,
socketio,
template_statistics_client,
)
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
@@ -34,18 +32,6 @@ from app.utils.user import user_has_permissions
from notifications_utils.recipients import format_phone_number_human_readable
@socketio.on("fetch_daily_stats")
def handle_fetch_daily_stats(service_id):
if service_id:
date_range = get_stats_date_range()
daily_stats = service_api_client.get_service_notification_statistics_by_day(
service_id, start_date=date_range["start_date"], days=date_range["days"]
)
emit("daily_stats_update", daily_stats)
else:
emit("error", {"error": "No service_id provided"})
@main.route("/services/<uuid:service_id>/dashboard")
@user_has_permissions("view_activity", "send_messages")
def old_service_dashboard(service_id):
@@ -98,7 +84,6 @@ def service_dashboard(service_id):
partials=get_dashboard_partials(service_id),
job_and_notifications=job_and_notifications,
service_data_retention_days=service_data_retention_days,
service_id=service_id,
)
@@ -449,24 +434,6 @@ def get_months_for_financial_year(year, time_format="%B"):
return [month.strftime(time_format) for month in (get_months_for_year(1, 13, year))]
def get_current_month_for_financial_year(year):
current_month = datetime.now().month
return current_month
def get_stats_date_range():
current_financial_year = get_current_financial_year()
current_month = get_current_month_for_financial_year(current_financial_year)
start_date = datetime.now().strftime("%Y-%m-%d")
days = 7
return {
"current_financial_year": current_financial_year,
"current_month": current_month,
"start_date": start_date,
"days": days,
}
def get_months_for_year(start, end, year):
return [datetime(year, month, 1) for month in range(start, end)]

View File

@@ -1,6 +1,6 @@
import os
from flask import abort, current_app, redirect, render_template, request, url_for
from flask import abort, redirect, render_template, request, url_for
from flask_login import current_user
from app import status_api_client
@@ -9,28 +9,20 @@ from app.main import main
from app.main.views.pricing import CURRENT_SMS_RATE
from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav
from app.utils.user import user_is_logged_in
from notifications_utils.url_safe_token import generate_token
login_dot_gov_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
@main.route("/")
def index():
if current_user and current_user.is_authenticated:
return redirect(url_for("main.choose_account"))
token = generate_token(
str(request.remote_addr),
current_app.config["SECRET_KEY"],
current_app.config["DANGEROUS_SALT"],
)
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
# handle unit tests
if url is not None:
url = url.replace("NONCE", token)
url = url.replace("STATE", token)
return render_template(
"views/signedout.html",
sms_rate=CURRENT_SMS_RATE,
counts=status_api_client.get_count_of_live_services_and_organizations(),
initial_signin_url=url,
login_dot_gov_url=login_dot_gov_url,
)

View File

@@ -143,40 +143,11 @@ def view_notifications(service_id, message_type=None):
True: ["reference"],
False: [],
}.get(bool(current_service.api_keys)),
download_link_one_day=url_for(
download_link=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="one_day",
),
download_link_today=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="today",
),
download_link_three_day=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="three_day",
),
download_link_five_day=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="five_day",
),
download_link_seven_day=url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days="seven_day",
),
)
@@ -212,9 +183,10 @@ def get_notifications(service_id, message_type, status_override=None): # noqa
filter_args["status"] = set_status_filters(filter_args)
service_data_retention_days = None
search_term = request.form.get("to", "")
if message_type is not None:
service_data_retention_days = current_service.get_days_of_retention(
message_type, number_of_days="seven_day"
message_type
)
if request.path.endswith("csv") and current_user.has_permissions("view_activity"):
@@ -240,6 +212,7 @@ def get_notifications(service_id, message_type, status_override=None): # noqa
)
url_args = {"message_type": message_type, "status": request.args.get("status")}
prev_page = None
if "links" in notifications and notifications["links"].get("prev", None):
prev_page = generate_previous_dict(
"main.view_notifications", service_id, page, url_args=url_args
@@ -260,6 +233,7 @@ def get_notifications(service_id, message_type, status_override=None): # noqa
)
else:
download_link = None
return {
"service_data_retention_days": service_data_retention_days,
"counts": render_template(
@@ -388,7 +362,6 @@ def get_job_partials(job):
filter_args = parse_filter_args(request.args)
filter_args["status"] = set_status_filters(filter_args)
notifications = job.get_notifications(status=filter_args["status"])
number_of_days = "seven_day"
counts = render_template(
"partials/count.html",
counts=_get_job_counts(job),
@@ -398,7 +371,7 @@ def get_job_partials(job):
),
)
service_data_retention_days = current_service.get_days_of_retention(
job.template_type, number_of_days
job.template_type
)
if request.referrer is not None:

View File

@@ -137,9 +137,9 @@ def get_all_personalisation_from_notification(notification):
def download_notifications_csv(service_id):
filter_args = parse_filter_args(request.args)
filter_args["status"] = set_status_filters(filter_args)
number_of_days = request.args["number_of_days"]
service_data_retention_days = current_service.get_days_of_retention(
filter_args.get("message_type")[0], number_of_days
filter_args.get("message_type")[0]
)
file_time = datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
file_time = f"{file_time} {get_user_preferred_timezone()}"

View File

@@ -26,7 +26,6 @@ from app.main.views import sign_in
from app.main.views.verify import activate_user
from app.models.user import InvitedOrgUser, InvitedUser, User
from app.utils import hide_from_search_engines, hilite
from app.utils.user import is_gov_user
@main.route("/register", methods=["GET", "POST"])
@@ -116,10 +115,10 @@ def registration_continue():
def get_invite_data_from_redis(state):
invite_data = json.loads(redis_client.get(f"invitedata-{state}"))
user_email = redis_client.get(f"user_email-{state}").decode("utf8")
user_uuid = redis_client.get(f"user_uuid-{state}").decode("utf8")
invited_user_email_address = redis_client.get(
invite_data = json.loads(redis_client.raw_get(f"invitedata-{state}"))
user_email = redis_client.raw_get(f"user_email-{state}").decode("utf8")
user_uuid = redis_client.raw_get(f"user_uuid-{state}").decode("utf8")
invited_user_email_address = redis_client.raw_get(
f"invited_user_email_address-{state}"
).decode("utf8")
return invite_data, user_email, user_uuid, invited_user_email_address
@@ -130,10 +129,10 @@ def put_invite_data_in_redis(
):
ttl = 60 * 15 # 15 minutes
redis_client.set(f"invitedata-{state}", json.dumps(invite_data), ex=ttl)
redis_client.set(f"user_email-{state}", user_email, ex=ttl)
redis_client.set(f"user_uuid-{state}", user_uuid, ex=ttl)
redis_client.set(
redis_client.raw_set(f"invitedata-{state}", json.dumps(invite_data), ex=ttl)
redis_client.raw_set(f"user_email-{state}", user_email, ex=ttl)
redis_client.raw_set(f"user_uuid-{state}", user_uuid, ex=ttl)
redis_client.raw_set(
f"invited_user_email_address-{state}",
invited_user_email_address,
ex=ttl,
@@ -148,11 +147,6 @@ def check_invited_user_email_address_matches_expected(
flash("You cannot accept an invite for another person.")
abort(403)
if not is_gov_user(user_email):
debug_msg("invited user has a non-government email address.")
flash("You must use a government email address.")
abort(403)
@main.route("/set-up-your-profile", methods=["GET", "POST"])
@hide_from_search_engines
@@ -163,7 +157,7 @@ def set_up_your_profile():
state = request.args.get("state")
login_gov_error = request.args.get("error")
if redis_client.get(f"invitedata-{state}") is None:
if redis_client.raw_get(f"invitedata-{state}") is None:
access_token = sign_in._get_access_token(code, state)
debug_msg("Got the access token for login.gov")
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
@@ -195,7 +189,7 @@ def set_up_your_profile():
if (
form.validate_on_submit()
and redis_client.get(f"invitedata-{state}") is not None
and redis_client.raw_get(f"invitedata-{state}") is not None
):
invite_data, user_email, user_uuid, invited_user_email_address = (
get_invite_data_from_redis(state)
@@ -251,21 +245,10 @@ def get_invited_user_email_address(invited_user_id):
def invited_user_accept_invite(invited_user_id):
invited_user = InvitedUser.by_id(invited_user_id)
if invited_user.status == "expired":
current_app.logger.error("User invitation has expired")
flash(
"Your invitation has expired; please contact the person who invited you for additional help."
)
flash("Your invitation has expired.")
abort(401)
if invited_user.status == "cancelled":
current_app.logger.error("User invitation has been cancelled")
flash(
"Your invitation is no longer valid; please contact the person who invited you for additional help."
)
abort(401)
invited_user.accept_invite()

View File

@@ -3,16 +3,7 @@ import uuid
from string import ascii_uppercase
from zipfile import BadZipFile
from flask import (
abort,
current_app,
flash,
redirect,
render_template,
request,
session,
url_for,
)
from flask import abort, flash, redirect, render_template, request, session, url_for
from flask_login import current_user
from markupsafe import Markup
from notifications_python_client.errors import HTTPError
@@ -40,12 +31,7 @@ from app.s3_client.s3_csv_client import (
s3upload,
set_metadata_on_csv_upload,
)
from app.utils import (
PermanentRedirect,
hilite,
should_skip_template_page,
unicode_truncate,
)
from app.utils import PermanentRedirect, should_skip_template_page, unicode_truncate
from app.utils.csv import Spreadsheet, get_errors_for_csv
from app.utils.templates import get_template
from app.utils.user import user_has_permissions
@@ -667,17 +653,18 @@ def preview_job(service_id, template_id, upload_id, row_index=2):
data = _check_messages(
service_id, template_id, upload_id, row_index, force_hide_sender=True
)
create_job(service_id, upload_id)
return render_template(
"views/check/preview.html",
scheduled_for=session["scheduled_for"],
scheduled_for=session.get("scheduled_for"),
**data,
)
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
@main.route("/services/<uuid:service_id>/create-job/<uuid:upload_id>", methods=["POST"])
@user_has_permissions("send_messages", restrict_admin_usage=True)
def start_job(service_id, upload_id):
def create_job(service_id, upload_id):
scheduled_for = session.pop("scheduled_for", None)
job_api_client.create_job(
upload_id,
@@ -685,8 +672,12 @@ def start_job(service_id, upload_id):
scheduled_for=scheduled_for,
)
session.pop("sender_id", None)
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
@user_has_permissions("send_messages", restrict_admin_usage=True)
def start_job(service_id, upload_id):
job_api_client.start_job(service_id, upload_id)
session.pop("sender_id", None)
return redirect(
url_for(
"main.view_job",
@@ -922,36 +913,7 @@ def preview_notification(service_id, template_id):
template_id=template_id,
)
)
session["scheduled_for"] = request.form.get("scheduled_for", "")
return render_template(
"views/notifications/preview.html",
**_check_notification(
service_id, template_id, show_recipient=False, force_hide_sender=True
),
scheduled_for=session["scheduled_for"],
recipient=recipient,
)
@main.route(
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
methods=["POST"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
def send_notification(service_id, template_id):
scheduled_for = session.pop("scheduled_for", "")
recipient = get_recipient()
if not recipient:
return redirect(
url_for(
".send_one_off",
service_id=service_id,
template_id=template_id,
)
)
keys = []
values = []
for k, v in session["placeholders"].items():
@@ -962,17 +924,9 @@ def send_notification(service_id, template_id):
vals = ",".join(values)
data = f"{data}\r\n{vals}"
filename = (
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
)
filename = f"one-off-{current_user.name}-{uuid.uuid4()}.csv"
my_data = {"filename": filename, "template_id": template_id, "data": data}
upload_id = s3upload(service_id, my_data)
# To debug messages that the user reports have not been sent, we log
# the csv filename and the job id. The user will give us the file name,
# so we can search on that to obtain the job id, which we can use elsewhere
# on the API side to find out what happens to the message.
current_app.logger.info(hilite(f"One-off file: {filename} job_id: {upload_id}"))
form = CsvUploadForm()
form.file.data = my_data
form.file.name = filename
@@ -990,10 +944,39 @@ def send_notification(service_id, template_id):
notification_count=1,
valid="True",
)
session["recipient"] = recipient
session["upload_id"] = upload_id
session["scheduled_for"] = request.form.get("scheduled_for", "")
session.pop("recipient")
session.pop("placeholders")
return render_template(
"views/notifications/preview.html",
**_check_notification(
service_id, template_id, show_recipient=False, force_hide_sender=True
),
upload_id=upload_id,
scheduled_for=session["scheduled_for"],
recipient=recipient,
)
@main.route(
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
methods=["POST"],
)
@user_has_permissions("send_messages", restrict_admin_usage=True)
def send_notification(service_id, template_id):
recipient = get_recipient()
if not recipient:
return redirect(
url_for(
".send_one_off",
service_id=service_id,
template_id=template_id,
)
)
upload_id = session.pop("upload_id")
job_api_client.start_job(service_id, upload_id)
# We have to wait for the job to run and create the notification in the database
time.sleep(0.1)
notifications = notification_api_client.get_notifications_for_service(
@@ -1022,17 +1005,15 @@ def send_notification(service_id, template_id):
job_id=upload_id,
)
)
total = notifications["total"]
current_app.logger.info(
hilite(
f"job_id: {upload_id} has notifications: {total} and attempts: {attempts}"
)
)
session.pop("recipient")
session.pop("placeholders")
return redirect(
url_for(
".view_job",
service_id=service_id,
job_id=upload_id,
from_job=upload_id,
notification_id=notifications["notifications"][0]["id"],
# used to show the final step of the tour (help=3) or not show
# a back link on a just sent one off notification (help=0)
help=request.args.get("help"),

View File

@@ -4,16 +4,7 @@ import uuid
import jwt
import requests
from flask import (
Response,
abort,
current_app,
flash,
redirect,
render_template,
request,
url_for,
)
from flask import Response, current_app, redirect, render_template, request, url_for
from flask_login import current_user
from app import login_manager, user_api_client
@@ -24,7 +15,6 @@ from app.models.user import User
from app.utils import hide_from_search_engines
from app.utils.login import is_safe_redirect_url
from app.utils.time import is_less_than_days_ago
from app.utils.user import is_gov_user
from notifications_utils.url_safe_token import generate_token
@@ -98,12 +88,6 @@ def _do_login_dot_gov():
try:
access_token = _get_access_token(code, state)
user_email, user_uuid = _get_user_email_and_uuid(access_token)
if not is_gov_user(user_email):
current_app.logger.error(
"invited user has a non-government email address."
)
flash("You must use a government email address.")
abort(403)
redirect_url = request.args.get("next")
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)

View File

@@ -32,7 +32,7 @@ def using_notify_nav():
"link": "main.trial_mode_new",
},
{
"name": "Tracking usage",
"name": "Pricing",
"link": "main.pricing",
},
{

View File

@@ -66,7 +66,7 @@ def activate_user(user_id):
user = User.from_id(user_id)
# TODO add org invites back in the new way
# organization_id = redis_client.get(
# organization_id = redis_client.raw_get(
# f"organization-invite-{user.email_address}"
# )
# user_api_client.add_user_to_organization(

View File

@@ -390,7 +390,7 @@ class Service(JSONModel, SortByNameMixin):
def get_data_retention_item(self, id):
return next((dr for dr in self.data_retention if dr["id"] == id), None)
def get_days_of_retention(self, notification_type, number_of_days):
def get_days_of_retention(self, notification_type):
return next(
(
dr
@@ -398,10 +398,7 @@ class Service(JSONModel, SortByNameMixin):
if dr["notification_type"] == notification_type
),
{},
).get(
"days_of_retention",
current_app.config["ACTIVITY_STATS_LIMIT_DAYS"].get(number_of_days),
)
).get("days_of_retention", current_app.config["ACTIVITY_STATS_LIMIT_DAYS"])
@cached_property
def organization(self):

View File

@@ -147,5 +147,10 @@ class JobApiClient(NotifyAdminAPIClient):
url="/service/{}/job/{}/cancel".format(service_id, job_id), data={}
)
def start_job(self, service_id, job_id):
return self.post(
url="/service/{}/job/{}/start-job".format(service_id, job_id), data={}
)
job_api_client = JobApiClient()

View File

@@ -37,11 +37,10 @@ class NotificationApiClient(NotifyAdminAPIClient):
# we do not want in our logs, so we do a POST request instead of a GET
method = self.post if to else self.get
kwargs = {"data": params} if to else {"params": params}
if job_id:
return method(
url="/service/{}/job/{}/notifications".format(service_id, job_id),
**kwargs
**kwargs,
)
else:
if limit_days is not None:
@@ -100,5 +99,8 @@ class NotificationApiClient(NotifyAdminAPIClient):
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
)["count"]
def get_total_notification_message_parts_by_job_id(self, job_id):
return self.get(url="/notifications/{}/message_parts".format(job_id))
notification_api_client = NotificationApiClient()

View File

@@ -43,16 +43,6 @@ class ServiceAPIClient(NotifyAdminAPIClient):
params={"limit_days": limit_days},
)["data"]
def get_service_notification_statistics_by_day(
self, service_id, start_date=None, days=None
):
if start_date is None:
start_date = datetime.now().strftime("%Y-%m-%d")
return self.get(
"/service/{0}/statistics/{1}/{2}".format(service_id, start_date, days),
)["data"]
def get_services(self, params_dict=None):
"""
Retrieve a list of services.

View File

@@ -1,5 +1,3 @@
import os
import botocore
from boto3 import Session
from botocore.config import Config
@@ -31,17 +29,6 @@ def get_s3_object(
)
s3 = session.resource("s3", config=AWS_CLIENT_CONFIG)
obj = s3.Object(bucket_name, filename)
# This 'proves' that use of moto in the relevant tests in test_send.py
# mocks everything related to S3. What you will see in the logs is:
# Exception: CREATED AT <MagicMock name='resource().Bucket().creation_date' id='4665562448'>
#
# raise Exception(f"CREATED AT {_s3.Bucket(bucket_name).creation_date}")
if os.getenv("NOTIFY_ENVIRONMENT") == "test":
teststr = str(s3.Bucket(bucket_name).creation_date).lower()
if "magicmock" not in teststr:
raise Exception(
"Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3"
)
return obj

View File

@@ -28,7 +28,6 @@ def get_csv_upload(service_id, upload_id):
def s3upload(service_id, filedata):
upload_id = str(uuid.uuid4())
bucket_name, file_location, access_key, secret_key, region = get_csv_location(
service_id, upload_id

View File

@@ -47,7 +47,7 @@
{% endblock %}
{% block content %}
{% block flash_messages %}
{% include 'new/components/flash_messages.html' %}
{% include 'flash_messages.html' %}
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">

View File

@@ -1,4 +1,3 @@
{# This template is an old version #}
{% if help %}
{% include 'partials/tour.html' %}
{% else %}

View File

@@ -31,6 +31,7 @@
<meta property="og:image" content="/static/images/notify-og-image.png">
{# google #}
<script type="text/javascript" src="{{ asset_url('js/gtm_head.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/chart.umd.js') }}"></script>
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" />
{% if g.hide_from_search_engines %}
<meta name="robots" content="noindex" />

View File

@@ -0,0 +1,57 @@
{% extends "/new/base.html" %}
{% block per_page_title %}
{% block service_page_title %}{% endblock %}{% if current_service.name %} {{ current_service.name }}{% endif %}
{% block org_page_title %}{% endblock %}{% if current_org.name %} {{ current_org.name }}{% endif %}
{% endblock %}
{% block main %}
<div class="grid-container">
{% block serviceNavigation %}
{% if current_org.name %}
{% else %}
{% include "new/components/service_navigation.html" %}
{% endif %}
{% endblock %}
{#
The withnav_template can serve as a replacement for both settings_template and org_template.html.
The file service_navigation.html is included only in withnav_template. It's not used in settings_template. That is one out of the two differences between settings template and withnav template. As a result, when other templates extend settings_template, they include the serviceNavigation block but keep it empty. The settings_template.html is specifically used for these pages in the app: manage-users.html, service-settings.html, and user-profile.html.
In addition, serviceNavigation should be empty on templates that previously extended org_template. For templates that previously extended org_template.html, there's an addition of the orgNavBreadcrumb block.
{% block orgNavBreadcrumb %}
{% include "/new/components/org_nav_breadcrumb.html" %}
{% endblock %}
#}
{% if current_org.name %}
{% block orgNavBreadcrumb %}{% include "/new/components/org_nav_breadcrumb.html" %}{% endblock %}
{% endif %}
<div class="grid-row margin-top-5">
<div class="tablet:grid-col-3">
{% block sideNavigation %}
{% if org_navigation_links %}
{% include "/new/components/org_nav.html" %}
{% else %}
{% include "/new/components/main_nav.html" %}
{% endif %}
{#
Include settings_nav.html for child templates that previously extended settings_template.
Include "org_nav.html" for child templates that previously extended org_template html
#}
{% endblock %}
</div>
<div class="tablet:grid-col-9 tablet:padding-left-4">
{% block beforeContent %}
{% block backLink %}{% endblock %}
{% endblock %}
<main id="main-content" role="main" class="usa-prose site-prose margin-bottom-10">
{% block content %}
{% include 'flash_messages.html' %}
{% block maincolumn_content %}{% endblock %}
{% endblock %}
</main>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,36 +0,0 @@
{# This template is an old version #}
{% extends "admin_template.html" %}
{% block per_page_title %}
{% block service_page_title %}{% endblock %} {{ current_service.name }}
{% endblock %}
{% block main %}
<div class="grid-container">
{% include "service_navigation.html" %}
<div class="grid-row margin-top-5">
{% if help %}
<div class="tablet:grid-col-3">
{% else %}
<div class="tablet:grid-col-3">
{% endif %}
{% include "main_nav.html" %}
</div>
{% if help %}
<div class="grid-col-8">
{% else %}
<div class="tablet:grid-col-9 tablet:padding-left-4">
{% endif %}
{% block beforeContent %}
{% block backLink %}{% endblock %}
{% endblock %}
<main id="main-content" role="main" class="usa-prose site-prose margin-bottom-10">
{% block content %}
{% include 'flash_messages.html' %}
{% block maincolumn_content %}{% endblock %}
{% endblock %}
</main>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,4 +1,3 @@
{# This template is an old version #}
<nav class="navigation">
<ul>
<li><a class="usa-link{{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Usage</a></li>

View File

@@ -1,4 +1,3 @@
{# This template is an old version #}
{% macro navigation_service_name(service) %}
<div class="font-body-2xl text-bold">
{{ service.name }}

View File

@@ -1,4 +1,3 @@
{# This template is an old version #}
{% if help %}
{% include 'partials/tour.html' %}
{% else %}

View File

@@ -1,5 +1,4 @@
{# This template is an old version #}
{% extends "admin_template.html" %}
{% extends "base.html" %}
{% block per_page_title %}
{% block service_page_title %}{% endblock %} {{ current_service.name }}

View File

@@ -22,8 +22,6 @@
Messages sent
</h2>
<!-- <button id="sevenDaysButton">7 Days</button>
<canvas id="myChart" data-service-id="{{ service_id }}"></canvas> -->
{{ ajax_block(partials, updates_url, 'inbox') }}
{{ ajax_block(partials, updates_url, 'totals') }}
@@ -61,7 +59,7 @@
{% set notification = job.notifications[0] %}
<tr class="table-row" id="{{ job.job_id }}">
<td class="table-field file-name">
{{ notification.job.original_file_name[:12] if notification.job.original_file_name else 'Manually entered number'}}
{{ notification.job.original_file_name if notification.job.original_file_name else 'Manually entered number'}}
<br>
<a class="usa-link file-list-filename" href="{{ job.view_job_link }}">View Batch</a>
</td>

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "settings_template.html" %}
{% from "components/tick-cross.html" import tick_cross %}
{% from "components/live-search.html" import live_search %}
{% from "components/components/button/macro.njk" import usaButton %}
@@ -7,12 +7,6 @@
Team members
{% endblock %}
{% block serviceNavigation %}{% endblock %}
{% block sideNavigation %}
{% include "/new/components/settings_nav.html" %}
{% endblock %}
{% block maincolumn_content %}
<div class="button-flex-header">

View File

@@ -9,13 +9,9 @@
<h1 class="font-body-2xl margin-bottom-3">Delivery status</h1>
<p>Notify starts sending your text messages immediately. Each message is sent to its cell phone carrier, which attempts
delivery to the recipient. This process is often almost instantaneous, but can sometimes take a while if a phone is
unavailable. The carrier will continue to try to deliver the message for up to 72 hours.</p>
<p>The Notify dashboard provides a high-level view of the number of messages Sent, Pending, Delivered, and Failed. The
dashboard data starts to update about five minutes after a message is sent and will continue to update for four hours.</p>
<p>Delivery statuses reflect delivery to a specific <em>cell phone number</em>, not to a <em>person</em>. No status can confirm that a
specific person actually received or read the message.</p>
<p>Notifys real-time dashboard lets you check the status of any message.</p>
<p>For <a class="usa-link" href="{{ url_for('main.security') }}">security</a>, this information is only available for seven days after a message has been sent. You can download a report, including a list of sent messages, for your own records.</p>
<p>This page describes the statuses you'll see when you're signed in to Notify.</p>
<!-- <p>If youre using the Notify API, read our <a class="usa-link" href="{{ url_for('.documentation') }}">documentation</a> for a list of API statuses.<p>
@@ -49,7 +45,7 @@
<p>Notify cannot tell you if your users open an email or click on the links in an email. We do not track open rates and click-throughs because there are privacy issues. Tracking emails without asking permission from users could breach General Data Protection Regulations (GDPR).</p> -->
<h2 id="text-message-statuses" class="heading-medium">Text messages</h2>
<div class="margin-bottom-8">
<div class="bottom-gutter-3-2">
{% call mapping_table(
caption='Message statuses text messages',
field_headings=['Status', 'Description'],
@@ -57,14 +53,12 @@
caption_visible=False
) %}
{% for message_status, description in [
('Scheduled', 'The total number of messages that have been scheduled to be sent at some future time.'),
('Canceled', 'Messages that were created and scheduled, but canceled prior to Sending.'),
('Total', 'The total number of messages that have been sent during the specified time.'),
('Pending', 'Notify has sent the message to the provider. The provider will try to deliver the message to the recipient for up to 72
hours. “Pending” indicates that Notify is waiting for delivery information.'),
('Total', 'The total number of messages that have been sent during the last seven days.'),
('Pending', 'Notify has sent the message to the provider. The provider will try to deliver the message to the recipient for up to 72 hours. Notify is waiting for delivery information.'),
('Delivered', 'The message was successfully delivered. Notify cannot tell you if a user has opened or read a message.'),
('Failed', 'The message could not be delivered.'),
('Process error / Delivery not attempted', 'If you receive a large number of process errors, please contact the Notify team.'),
('Failed', 'The provider could not deliver the message. This can happen if the phone number was wrong or if the network operator
rejects the message. If youre sure that these phone numbers are correct, you should <a class="usa-link" href="/support">contact us</a>. If not, you should remove them from your database. Youll still be charged for text messages that
cannot be delivered.' | safe),
] %}
{% call row() %}
{{ text_field(message_status) }}
@@ -73,21 +67,5 @@
{% endfor %}
{% endcall %}
</div>
<h2 class="heading-medium">About carrier statuses</h2>
<p>Sometimes Notify receives more detailed information from the carriers on the status of messages, and these can be found
in the downloadable reports. Not all carriers provide the same level of detail regarding delivery and some delivery
statutes have a slight variation in word choice. Notify includes this information in the reports to provide you as much
detail as possible. Remember, for <a class="usa-link" href="/features/security">security</a> purposes, detailed information is only available for seven days after a
message has been sent.</p>
<h2 class="heading-medium">Opting out</h2>
<p>A text recipient can opt out of receiving text messages from your phone number at any time by responding “STOP” or
“QUIT” or <a class="usa-link" href="https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-sms-limitations-opt-out.html#settings-sms-self-opt-out-terms">one of several other keywords</a>. If they opt out,</p>
<ul>
<li>They may receive a response from their carrier stating that they will no longer receive text messages from that number.</li>
<li>Any subsequent messages sent to that number will not be delivered (unless the recipient opts back in by texting “START”
or “OPT-IN”).</li>
</ul>
<p>Notify.gov does not yet have a way to pull opt-out status and make it available in the UI for agencies to download.</p>
{% endblock %}

View File

@@ -64,22 +64,10 @@
{% if current_user.has_permissions('view_activity') %}
<p class="font-body-sm">
<a href="{{ download_link_seven_day }}" download="download" class="usa-link">Download all data last 7 days (<abbr title="Comma separated values">CSV</abbr>)</a>
<a href="{{ download_link }}" download="download" class="usa-link">Download this report (<abbr title="Comma separated values">CSV</abbr>)</a>
&emsp;
Data available for {{ partials.service_data_retention_days }} days
</p>
<p class="font-body-sm">
<a href="{{ download_link_five_day }}" download="download" class="usa-link">Download all data last 5 days (<abbr title="Comma separated values">CSV</abbr>)</a>
&emsp;
</p>
<p class="font-body-sm">
<a href="{{ download_link_three_day }}" download="download" class="usa-link">Download all data last 3 days (<abbr title="Comma separated values">CSV</abbr>)</a>
&emsp;
</p>
<p class="font-body-sm">
<a href="{{ download_link_today }}" download="download" class="usa-link">Download all data today (<abbr title="Comma separated values">CSV</abbr>)</a>
&emsp;
</p>
{% endif %}
{{ ajax_block(

View File

@@ -61,6 +61,7 @@
)}}" class='page-footer'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<!-- <p>Placeholder: This message will be delivered to <b>400 phone numbers</b> and will use a total of <b>800 message parts</b>, leaving Washington DSHS with <b>249,200 message parts remaining</b>.</p> -->
<h3>Does everything look good?</h3>
{% if not error %}
{% set button_text %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/page-header.html" import page_header %}
{% block org_page_title %}

View File

@@ -2,7 +2,7 @@
{% from "components/big-number.html" import big_number %}
{% from "components/live-search.html" import live_search %}
{% from "components/pill.html" import pill %}
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% block org_page_title %}
Usage

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/page-header.html" import page_header %}
{% from "components/list-entry.html" import list_entry %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/form.html" import form_wrapper %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/page-header.html" import page_header %}
{% from "components/form.html" import form_wrapper %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/table.html" import mapping_table, optional_text_field, row, text_field, edit_field with context %}
{% block org_page_title %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/live-search.html" import live_search %}
{% block org_page_title %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/table.html" import list_table, row, field, hidden_field_heading %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/live-search.html" import live_search %}

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "org_template.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/page-header.html" import page_header %}

View File

@@ -45,7 +45,7 @@
{% block backLink %}{% endblock %}
<main id="main-content" role="main">
{% block content %}
{% include 'new/components/flash_messages.html' %}
{% include 'flash_messages.html' %}
{% block platform_admin_content %}{% endblock %}
{% endblock %}
</main>

View File

@@ -15,7 +15,7 @@ Message parts
{{ content_metadata(
data={
"Last updated": "April 10, 2024"
"Last updated": "February 5, 2024"
}
) }}
@@ -26,15 +26,13 @@ more parts towards the allowance if you:</p>
<ul class="list list-bullet">
<li>send text messages longer than 160 characters</a></li>
<li>use certain <a class="usa-link" href="#symbols">signs and symbols</a></li>
<li>use <a class="usa-link" href="#accents">accents and accented letters, including non-romanized scripts</a></li>
<li>use <a class="usa-link" href="#accents">accents and accented letters</a></li>
</ul>
<h3 class="font-body-lg" id="long-text-messages">Long text messages</h3>
<p>If a text message is longer than 160 characters (including spaces and service name), it counts as more than one message
part.</p>
<h4>Calculation of message parts without special characters</h4>
<div class="bottom-gutter-3-2">
{% call mapping_table(
caption='Text message pricing',
@@ -74,17 +72,12 @@ and the number of parts youll have left.</p>
<p>Using them can increase the cost of sending text messages.</p>
<h3 class="font-body-lg" id="accents">Accented characters and non-romanized scripts</h3>
<p>Notify can handle a wide range of different languages and scripts. However, occasionally some phone carriers may
struggle to display special characters or non-romanized scripts. (Languages such as Arabic, Chinese, Japanese, Korean,
and Russian use non-romanized scripts with different characters.) Best practices encourage communication in the
recipients preferred language, but we are aware that, rarely, a phone carrier will not be able to handle the message.</p>
<h3 class="font-body-lg" id="accents">Accents and accented characters</h3>
<p>Some languages use accented characters.</p>
<p>The following accented characters do not affect the cost of sending text messages: Ä, É, Ö, Ü, à, ä, é, è, ì, ò, ö,
ù, ü.</p>
<p>Using other accented characters or scripts will increase the cost of sending text messages. Even one accented character
(with the exception of those noted above), or use of a non-romanized or logographic script will cause the entire message
to be calculated as detailed below.<p>
<h4>Calculation of message parts with special characters or non-romanized scripts</h4>
<p>Using other accented characters can increase the cost of sending text messages.
<p>
{% set accentedChars %}
<div class="bottom-gutter-3-2">
{% call mapping_table(

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "settings_template.html" %}
{% from "components/banner.html" import banner_wrapper %}
{% from "components/table.html" import mapping_table, row, settings_row, text_field, optional_text_field, edit_field, field, boolean_field with context %}
@@ -6,12 +6,6 @@
Settings
{% endblock %}
{% block serviceNavigation %}{% endblock %}
{% block sideNavigation %}
{% include "/new/components/settings_nav.html" %}
{% endblock %}
{% block maincolumn_content %}
<h1 class="font-body-lg">Settings</h1>

View File

@@ -21,8 +21,7 @@ Notify.gov
<h1 class="font-serif-2xl usa-hero__heading">Reach people where they are with government-powered text messages</h1>
<p class="font-sans-lg">Notify.gov is a text message service that helps federal, state, local, tribal and territorial governments more effectively communicate with the people they serve.</p>
<div class="usa-button-group margin-bottom-5">
<a class="usa-button usa-button login-button login-button--primary margin-right-2" href="{{ initial_signin_url }}">Sign in with <img src="{{ asset_url('images/logo-login.svg') }}" alt="Login.gov logo">
</a>
<a class="usa-button usa-button--big margin-right-2" href="{{ url_for('main.sign_in' ) }}">Sign in</a>
if you are an existing pilot partner
</div>
<p class="font-sans-md">Currently we are only working with select pilot partners. If you are interested in using Notify.gov in the future, please contact <br><a href="mailto:tts-benefits-studio@gsa.gov">tts-benefits-studio@gsa.gov</a> to learn more.</p>

View File

@@ -32,6 +32,22 @@
<a class="usa-link usa-button" href="{{ initial_signin_url }}">Sign in with Login.gov</a>
{% endif %}
</div>
</div>
<div class="tablet:grid-col-6 tablet:grid-offset-1 margin-top-2 padding-y-2 padding-x-4 bg-base-lightest">
<h2 class="font-body-lg">Effective April 16, 2024 Notify.gov requires you sign-in through Login.gov</h2>
<p>Why are we doing this?</p>
<ul class="usa-list">
<li><strong>Enhanced security:</strong> Login.gov is really secure and trustworthy</li>
<li><strong>One single source for signing in:</strong> You can use Login.gov for other services within the federal government</li>
<li><strong>2FA flexibility:</strong> Login.gov supports multiple methods for users to verify their identity.</li>
</ul>
<p>What do I need to do?</p>
<ul class="usa-list">
<li>If you have a Login.gov account, start using it to sign in to Notify today.</li>
<li>If you dont have a Login.gov account, you must create one to continue to access Notify.</li>
</ul>
<div class="border-bottom border-base-lighter margin-y-4"></div>
<a class="usa-link usa-button usa-button--outline margin-bottom-3" href="{{ initial_signin_url }}">Create Login.gov account</a>
</div>
</div>
{% endblock %}

View File

@@ -21,7 +21,7 @@
<li>A text message of 160-306 characters is two parts.</li>
</ul>
<p>For more information on how message parts are calculated, see
<a href="/using-notify/pricing">Tracking usage</a>.</p>
<a href="/using-notify/pricing">Pricing</a>.</p>
<h2 class="font-body-lg">Before going Live</h2>
<p>Before you request to make your service live so you can send messages to clients:</p>

View File

@@ -1,4 +1,4 @@
{% extends "withnav_template.html" %}
{% extends "settings_template.html" %}
{% from "components/table.html" import list_table, row, field %}
{% from "components/table.html" import mapping_table, row, text_field, optional_text_field, edit_field, field, boolean_field with context %}
@@ -6,12 +6,6 @@
User profile
{% endblock %}
{% block serviceNavigation %}{% endblock %}
{% block sideNavigation %}
{% include "/new/components/settings_nav.html" %}
{% endblock %}
{% block maincolumn_content %}
<h1 class="font-body-2xl margin-bottom-3">User profile</h1>

View File

@@ -1,56 +1,31 @@
{% extends "base.html" %}
{% block per_page_title %}
{% if current_org.name %}
{% block org_page_title %}{% endblock %} {{ current_org.name }}
{% else %}
{% block service_page_title %}{% endblock %} {{ current_service.name }}
{% endif %}
{% block service_page_title %}{% endblock %} {{ current_service.name }}
{% endblock %}
{% block main %}
<div class="grid-container">
{% block serviceNavigation %}
{% if current_org.name %}
{% else %}
{% include "new/components/service_nav.html" %}
{% endif %}
{% endblock %}
{#
The withnav_template can serve as a replacement for both settings_template and org_template.html.
The file service_navigation.html is included only in withnav_template. It's not used in settings_template. That is one out of the two differences between settings template and withnav template. As a result, when other templates extend settings_template, they include the serviceNavigation block but keep it empty. The settings_template.html is specifically used for these pages in the app: manage-users.html, service-settings.html, and user-profile.html.
In addition, serviceNavigation should be empty on templates that previously extended org_template. For templates that previously extended org_template.html, there's an addition of the orgNavBreadcrumb block.
{% block orgNavBreadcrumb %}
{% include "/new/components/org_nav_breadcrumb.html" %}
{% endblock %}
#}
{% if current_org.name %}
{% block orgNavBreadcrumb %}{% include "/new/components/org_nav_breadcrumb.html" %}{% endblock %}
{% endif %}
{% include "service_navigation.html" %}
<div class="grid-row margin-top-5">
<div class="tablet:grid-col-3">
{% block sideNavigation %}
{% if current_org.name %}
{% include "/new/components/org_nav.html" %}
{% else %}
{% include "/new/components/main_nav.html" %}
{% endif %}
{#
Include settings_nav.html for child templates that previously extended settings_template.
Include "org_nav.html" for child templates that previously extended org_template html
#}
{% endblock %}
</div>
<div class="tablet:grid-col-9 tablet:padding-left-4">
{% if help %}
<div class="tablet:grid-col-3">
{% else %}
<div class="tablet:grid-col-3">
{% endif %}
{% include "main_nav.html" %}
</div>
{% if help %}
<div class="grid-col-8">
{% else %}
<div class="tablet:grid-col-9 tablet:padding-left-4">
{% endif %}
{% block beforeContent %}
{% block backLink %}{% endblock %}
{% endblock %}
<main id="main-content" role="main" class="usa-prose site-prose margin-bottom-10">
{% block content %}
{% include 'new/components/flash_messages.html' %}
{% include 'flash_messages.html' %}
{% block maincolumn_content %}{% endblock %}
{% endblock %}
</main>

View File

@@ -1,11 +1,9 @@
import datetime
import pytz
from flask import current_app, json
from flask_login import current_user
from app.models.spreadsheet import Spreadsheet
from app.utils import hilite
from app.utils.templates import get_sample_template
from notifications_utils.recipients import RecipientCSV
@@ -73,23 +71,7 @@ def generate_notifications_csv(**kwargs):
# This generates the "batch" csv report
if kwargs.get("job_id"):
# Some unit tests are mocking the kwargs and turning them into a function instead of dict,
# hence the try/except.
try:
current_app.logger.info(
hilite(f"Setting up report with kwargs {json.dumps(kwargs)}")
)
except TypeError:
pass
original_file_contents = s3download(kwargs["service_id"], kwargs["job_id"])
# This will verify that the user actually did successfully upload a csv for a one-off. Limit the size
# we display to 999 characters, because we don't want to show the contents for reports with thousands of rows.
current_app.logger.info(
hilite(
f"Original csv for job_id {kwargs['job_id']}: {original_file_contents[0:999]}"
)
)
original_upload = RecipientCSV(
original_file_contents,
template=get_sample_template(kwargs["template_type"]),

View File

@@ -4,6 +4,7 @@ from flask import abort, current_app
from flask_login import current_user, login_required
from app import config
from app.notify_client.organizations_api_client import organizations_client
user_is_logged_in = login_required
@@ -50,7 +51,7 @@ def user_is_platform_admin(f):
def is_gov_user(email_address):
return _email_address_ends_with(
email_address, config.Config.GOVERNMENT_EMAIL_DOMAIN_NAMES
) # or _email_address_ends_with(email_address, organizations_client.get_domains())
) or _email_address_ends_with(email_address, organizations_client.get_domains())
def _email_address_ends_with(email_address, known_domains):

View File

@@ -1,6 +1,6 @@
env: production
instances: 2
memory: 2G
memory: 1G
public_admin_route: beta.notify.gov
cloud_dot_gov_route: notify.app.cloud.gov
redis_enabled: 1

View File

@@ -96,9 +96,7 @@ const javascripts = () => {
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
paths.npm + 'timeago/jquery.timeago.js',
paths.npm + 'textarea-caret/index.js',
paths.npm + 'cbor-js/cbor.js',
paths.npm + 'socket.io-client/dist/socket.io.min.js',
paths.npm + 'chart.js/dist/chart.umd.js'
paths.npm + 'cbor-js/cbor.js'
]));
// JS local to this application
@@ -127,7 +125,7 @@ const javascripts = () => {
paths.src + 'javascripts/date.js',
paths.src + 'javascripts/loginAlert.js',
paths.src + 'javascripts/main.js',
paths.src + 'javascripts/sampleChartDashboard.js',
paths.src + 'javascripts/chartDashboard.js',
])
.pipe(plugins.prettyerror())
.pipe(plugins.babel({

View File

@@ -1,14 +1,12 @@
import os
import socket
import sys
import traceback
import multiprocessing
import eventlet
import gunicorn
# Let gunicorn figure out the right number of workers
# The recommended formula is cpu_count() * 2 + 1
# but we have an unusual configuration with a lot of cpus and not much memory
# so adjust it.
workers = multiprocessing.cpu_count()
workers = 5
worker_class = "eventlet"
bind = "0.0.0.0:{}".format(os.getenv("PORT"))
disable_redirect_access_to_syslog = True
@@ -19,3 +17,21 @@ def worker_abort(worker):
worker.log.info("worker received ABORT")
for stack in sys._current_frames().values():
worker.log.error("".join(traceback.format_stack(stack)))
def fix_ssl_monkeypatching():
"""
eventlet works by monkey-patching core IO libraries (such as ssl) to be non-blocking. However, there's currently
a bug: In the normal socket library it may throw a timeout error as a `socket.timeout` exception. However
eventlet.green.ssl's patch raises an ssl.SSLError('timed out',) instead. redispy handles socket.timeout but not
ssl.SSLError, so we solve this by monkey patching the monkey patching code to raise the correct exception type
:scream:
https://github.com/eventlet/eventlet/issues/692
"""
# this has probably already been called somewhere in gunicorn internals, however, to be sure, we invoke it again.
# eventlet.monkey_patch can be called multiple times without issue
eventlet.monkey_patch()
eventlet.green.ssl.timeout_exc = socket.timeout
fix_ssl_monkeypatching()

View File

@@ -0,0 +1,55 @@
import requests
from flask import current_app
class AntivirusError(Exception):
def __init__(self, message=None, status_code=None):
self.message = message
self.status_code = status_code
@classmethod
def from_exception(cls, e):
try:
message = e.response.json()["error"]
status_code = e.response.status_code
except (TypeError, ValueError, AttributeError, KeyError):
message = "connection error"
status_code = 503
return cls(message, status_code)
class AntivirusClient:
def __init__(self, api_host=None, auth_token=None):
self.api_host = api_host
self.auth_token = auth_token
def init_app(self, app):
self.api_host = app.config["ANTIVIRUS_API_HOST"]
self.auth_token = app.config["ANTIVIRUS_API_KEY"]
def scan(self, document_stream):
try:
response = requests.post(
"{}/scan".format(self.api_host),
headers={
"Authorization": "Bearer {}".format(self.auth_token),
},
files={"document": document_stream},
)
response.raise_for_status()
except requests.RequestException as e:
error = AntivirusError.from_exception(e)
current_app.logger.warning(
"Notify Antivirus API request failed with error: {}".format(
error.message
)
)
raise error
finally:
document_stream.seek(0)
return response.json()["ok"]

View File

@@ -0,0 +1,86 @@
from base64 import urlsafe_b64encode
from json import dumps, loads
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from itsdangerous import BadSignature, URLSafeSerializer
class EncryptionError(Exception):
pass
class SaltLengthError(Exception):
pass
class Encryption:
def init_app(self, app):
self._serializer = URLSafeSerializer(app.config.get("SECRET_KEY"))
self._salt = app.config.get("DANGEROUS_SALT")
self._password = app.config.get("SECRET_KEY").encode()
try:
self._shared_encryptor = Fernet(self._derive_key(self._salt))
except SaltLengthError as reason:
raise EncryptionError(
"DANGEROUS_SALT must be at least 16 bytes"
) from reason
def encrypt(self, thing_to_encrypt, salt=None):
"""Encrypt a string or object
thing_to_encrypt must be serializable as JSON
Returns a UTF-8 string
"""
serialized_bytes = dumps(thing_to_encrypt).encode("utf-8")
encrypted_bytes = self._encryptor(salt).encrypt(serialized_bytes)
return encrypted_bytes.decode("utf-8")
def decrypt(self, thing_to_decrypt, salt=None):
"""Decrypt a UTF-8 string or bytes.
Once decrypted, thing_to_decrypt must be deserializable from JSON.
"""
try:
return loads(self._encryptor(salt).decrypt(thing_to_decrypt))
except InvalidToken as reason:
raise EncryptionError from reason
def sign(self, thing_to_sign, salt=None):
return self._serializer.dumps(thing_to_sign, salt=(salt or self._salt))
def verify_signature(self, thing_to_verify, salt=None):
try:
return self._serializer.loads(thing_to_verify, salt=(salt or self._salt))
except BadSignature as reason:
raise EncryptionError from reason
def _encryptor(self, salt=None):
if salt is None:
return self._shared_encryptor
else:
try:
return Fernet(self._derive_key(salt))
except SaltLengthError as reason:
raise EncryptionError(
"Custom salt value must be at least 16 bytes"
) from reason
def _derive_key(self, salt):
"""Derive a key suitable for use within Fernet from the SECRET_KEY and salt
* For the salt to be secure, it must be 16 bytes or longer and randomly generated.
* 600_000 was chosen for the iterations because it is what OWASP recommends as
* of [February 2023](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2)
* For more information, see https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#pbkdf2
* and https://cryptography.io/en/latest/fernet/#using-passwords-with-fernet
"""
salt_bytes = salt.encode()
if len(salt_bytes) < 16:
raise SaltLengthError
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(), length=32, salt=salt_bytes, iterations=600_000
)
return urlsafe_b64encode(kdf.derive(self._password))

View File

@@ -133,13 +133,19 @@ class RedisClient:
else:
return False
def raw_set(self, key, value, ex=None, px=None, nx=False, xx=False):
self.redis_store.set(key, value, ex, px, nx, xx)
def set(
self, key, value, ex=None, px=None, nx=False, xx=False, raise_exception=False
):
key = prepare_value(key)
value = prepare_value(value)
if self.active:
self.redis_store.set(key, value, ex, px, nx, xx)
try:
self.redis_store.set(key, value, ex, px, nx, xx)
except Exception as e:
self.__handle_exception(e, raise_exception, "set", key)
def incr(self, key, raise_exception=False):
key = prepare_value(key)
@@ -149,10 +155,16 @@ class RedisClient:
except Exception as e:
self.__handle_exception(e, raise_exception, "incr", key)
def raw_get(self, key):
return self.redis_store.get(key)
def get(self, key, raise_exception=False):
key = prepare_value(key)
if self.active:
return self.redis_store.get(key)
try:
return self.redis_store.get(key)
except Exception as e:
self.__handle_exception(e, raise_exception, "get", key)
return None

View File

@@ -0,0 +1,150 @@
import requests
from flask import current_app
class ZendeskError(Exception):
def __init__(self, response):
self.response = response
class ZendeskClient:
# the account used to authenticate with. If no requester is provided, the ticket will come from this account.
NOTIFY_ZENDESK_EMAIL = "zd-api-notify@digital.cabinet-office.gov.uk"
ZENDESK_TICKET_URL = "https://govuk.zendesk.com/api/v2/tickets.json"
def __init__(self):
self.api_key = None
def init_app(self, app, *args, **kwargs):
self.api_key = app.config.get("ZENDESK_API_KEY")
def send_ticket_to_zendesk(self, ticket):
response = requests.post(
self.ZENDESK_TICKET_URL,
json=ticket.request_data,
auth=(f"{self.NOTIFY_ZENDESK_EMAIL}/token", self.api_key),
)
if response.status_code != 201:
current_app.logger.error(
f"Zendesk create ticket request failed with {response.status_code} '{response.json()}'"
)
raise ZendeskError(response)
ticket_id = response.json()["ticket"]["id"]
current_app.logger.info(f"Zendesk create ticket {ticket_id} succeeded")
class NotifySupportTicket:
PRIORITY_URGENT = "urgent"
PRIORITY_HIGH = "high"
PRIORITY_NORMAL = "normal"
PRIORITY_LOW = "low"
TAGS_P2 = "govuk_notify_support"
TAGS_P1 = "govuk_notify_emergency"
TYPE_PROBLEM = "problem"
TYPE_INCIDENT = "incident"
TYPE_QUESTION = "question"
TYPE_TASK = "task"
# Group: 3rd Line--Notify Support
NOTIFY_GROUP_ID = 360000036529
# Organization: GDS
NOTIFY_ORG_ID = 21891972
NOTIFY_TICKET_FORM_ID = 1900000284794
def __init__(
self,
subject,
message,
ticket_type,
p1=False,
user_name=None,
user_email=None,
requester_sees_message_content=True,
technical_ticket=False,
ticket_categories=None,
org_id=None,
org_type=None,
service_id=None,
email_ccs=None,
):
self.subject = subject
self.message = message
self.ticket_type = ticket_type
self.p1 = p1
self.user_name = user_name
self.user_email = user_email
self.requester_sees_message_content = requester_sees_message_content
self.technical_ticket = technical_ticket
self.ticket_categories = ticket_categories or []
self.org_id = org_id
self.org_type = org_type
self.service_id = service_id
self.email_ccs = email_ccs
@property
def request_data(self):
data = {
"ticket": {
"subject": self.subject,
"comment": {
"body": self.message,
"public": self.requester_sees_message_content,
},
"group_id": self.NOTIFY_GROUP_ID,
"organization_id": self.NOTIFY_ORG_ID,
"ticket_form_id": self.NOTIFY_TICKET_FORM_ID,
"priority": self.PRIORITY_URGENT if self.p1 else self.PRIORITY_NORMAL,
"tags": [self.TAGS_P1 if self.p1 else self.TAGS_P2],
"type": self.ticket_type,
"custom_fields": self._get_custom_fields(),
}
}
if self.email_ccs:
data["ticket"]["email_ccs"] = [
{"user_email": email, "action": "put"} for email in self.email_ccs
]
# if no requester provided, then the call came from within Notify 👻
if self.user_email:
data["ticket"]["requester"] = {
"email": self.user_email,
"name": self.user_name or "(no name supplied)",
}
return data
def _get_custom_fields(self):
technical_ticket_tag = (
f'notify_ticket_type_{"" if self.technical_ticket else "non_"}technical'
)
org_type_tag = f"notify_org_type_{self.org_type}" if self.org_type else None
return [
{
"id": "1900000744994",
"value": technical_ticket_tag,
}, # Notify Ticket type field
{
"id": "360022836500",
"value": self.ticket_categories,
}, # Notify Ticket category field
{
"id": "360022943959",
"value": self.org_id,
}, # Notify Organisation ID field
{
"id": "360022943979",
"value": org_type_tag,
}, # Notify Organisation type field
{
"id": "1900000745014",
"value": self.service_id,
}, # Notify Service ID field
]

View File

@@ -1,6 +1,5 @@
import logging
import logging.handlers
import re
import sys
from itertools import product
@@ -70,7 +69,6 @@ def configure_handler(handler, app, formatter):
handler.addFilter(AppNameFilter(app.config["NOTIFY_APP_NAME"]))
handler.addFilter(RequestIdFilter())
handler.addFilter(ServiceIdFilter())
handler.addFilter(PIIFilter())
return handler
@@ -133,32 +131,3 @@ class JSONFormatter(BaseJSONFormatter):
except (KeyError, IndexError) as e:
logger.exception("failed to format log message: {} not found".format(e))
return log_record
class PIIFilter(logging.Filter):
def scrub(self, msg):
# Eventually we want to scrub all messages in all logs for phone numbers
# and email addresses, masking them. Ultimately this will probably get
# refactored into a 'SafeLogger' subclass or something, but let's start here
# with phones.
# Sometimes just an exception object is passed in for the message, skip those.
if not isinstance(msg, str):
return msg
phones = re.findall("(?:\\+ *)?\\d[\\d\\- ]{7,}\\d", msg)
phones = [phone.replace("-", "").replace(" ", "") for phone in phones]
for phone in phones:
msg = msg.replace(phone, "1XXXXXXXXXX")
emails = re.findall(
r"[\w\.-]+@[\w\.-]+", msg
) # ['alice@google.com', 'bob@abc.com']
for email in emails:
# do something with each found email string
masked_email = "XXXXX@XXXXXXX"
msg = msg.replace(email, masked_email)
return msg
def filter(self, record):
record.msg = self.scrub(record.msg)
return record

View File

@@ -38,17 +38,6 @@ def s3upload(
region_name=region,
)
_s3 = session.resource("s3", config=AWS_CLIENT_CONFIG)
# This 'proves' that use of moto in the relevant tests in test_send.py
# mocks everything related to S3. What you will see in the logs is:
# Exception: CREATED AT <MagicMock name='resource().Bucket().creation_date' id='4665562448'>
#
# raise Exception(f"CREATED AT {_s3.Bucket(bucket_name).creation_date}")
if os.getenv("NOTIFY_ENVIRONMENT") == "test":
teststr = str(_s3.Bucket(bucket_name).creation_date).lower()
if "magicmock" not in teststr:
raise Exception(
"Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3"
)
key = _s3.Object(bucket_name, file_location)
@@ -93,17 +82,6 @@ def s3download(
)
s3 = session.resource("s3", config=AWS_CLIENT_CONFIG)
key = s3.Object(bucket_name, filename)
# This 'proves' that use of moto in the relevant tests in test_send.py
# mocks everything related to S3. What you will see in the logs is:
# Exception: CREATED AT <MagicMock name='resource().Bucket().creation_date' id='4665562448'>
#
# raise Exception(f"CREATED AT {_s3.Bucket(bucket_name).creation_date}")
if os.getenv("NOTIFY_ENVIRONMENT") == "test":
teststr = str(s3.Bucket(bucket_name).creation_date).lower()
if "magicmock" not in teststr:
raise Exception(
"Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3"
)
return key.get()["Body"]
except botocore.exceptions.ClientError as error:
raise S3ObjectNotFound(error.response, error.operation_name)

1347
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,9 +21,6 @@
"author": "General Services Administration",
"license": "CC0",
"homepage": "https://github.com/GSA/notifications-admin#readme",
"overrides": {
"graceful-fs": "^4.2.11"
},
"dependencies": {
"@uswds/uswds": "^3.4.1",
"cbor-js": "0.1.0",
@@ -31,12 +28,11 @@
"govuk_frontend_toolkit": "8.1.0",
"govuk-frontend": "2.13.0",
"hogan": "1.0.2",
"jquery": "3.7.1",
"jquery": "3.6.1",
"morphdom": "2.6.1",
"python": "^0.0.4",
"query-command-supported": "1.0.0",
"sass-embedded": "^1.69.5",
"socket.io-client": "^4.2.0",
"textarea-caret": "3.1.0",
"timeago": "1.6.7"
},
@@ -45,8 +41,8 @@
"@babel/preset-env": "7.19.4",
"@uswds/compile": "^1.1.0",
"better-npm-audit": "^3.7.3",
"gulp": "^4.0.2",
"gulp-add-src": "^1.0.0",
"gulp": "4.0.2",
"gulp-add-src": "1.0.0",
"gulp-babel": "8.0.0",
"gulp-better-rollup": "4.0.1",
"gulp-clean-css": "4.3.0",

899
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ humanize = "~=4.9"
itsdangerous = "~=2.2"
jinja2 = "~=3.1"
newrelic = "*"
notifications-python-client = "==9.1.0"
notifications-python-client = "==9.0.0"
pyexcel = "==0.7.0"
pyexcel-io = "==0.6.6"
pyexcel-ods3 = "==0.6.1"
@@ -39,21 +39,21 @@ wtforms = "~=3.1"
markdown = "^3.5.2"
async-timeout = "^4.0.3"
bleach = "^6.1.0"
boto3 = "^1.34.128"
botocore = "^1.34.128"
boto3 = "^1.34.106"
botocore = "^1.34.106"
cachetools = "^5.3.3"
cffi = "^1.16.0"
cryptography = "^42.0.8"
cryptography = "^42.0.7"
flask-redis = "^0.4.0"
geojson = "^3.1.0"
jmespath = "^1.0.1"
mistune = "0.8.4"
numpy = "^1.26.4"
ordered-set = "^4.1.0"
phonenumbers = "^8.13.39"
phonenumbers = "^8.13.36"
pycparser = "^2.22"
python-json-logger = "^2.0.7"
redis = "^5.0.6"
redis = "^5.0.4"
regex = "^2024.5.15"
s3transfer = "^0.10.1"
shapely = "^2.0.4"
@@ -65,11 +65,10 @@ idna = "^3.7"
markupsafe = "^2.1.5"
python-dateutil = "^2.9.0.post0"
pyyaml = "^6.0.1"
requests = "^2.32.3"
requests = "^2.31.0"
six = "^1.16.0"
urllib3 = "^2.2.2"
urllib3 = "^2.2.1"
webencodings = "^0.5.1"
flask-socketio = "^5.3.6"
[tool.poetry.group.dev.dependencies]
@@ -83,10 +82,9 @@ flake8-print = "^5.0.0"
flake8-pytest-style = "^1.7.2"
isort = "^5.13.2"
jinja2-cli = {version = "==0.8.2", extras = ["yaml"]}
moto = "*"
pip-audit = "*"
pre-commit = "^3.7.1"
pytest = "^8.2.2"
pytest = "^8.1.1"
pytest-env = "^1.1.3"
pytest-mock = "^3.14.0"
pytest-playwright = "^0.5.0"
@@ -94,7 +92,6 @@ pytest-xdist = "^3.5.0"
radon = "^6.0.1"
requests-mock = "^1.11.0"
vulture = "^2.11"
detect-secrets = "^1.5.0"
[build-system]

View File

@@ -1 +1 @@
python-3.12.3
python-3.12.x

View File

@@ -1,9 +1,9 @@
terraform {
required_version = "~> 1.7"
required_version = "~> 1.0"
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.53.1"
version = "0.53.0"
}
}
}

View File

@@ -6,7 +6,7 @@ locals {
recursive_delete = false
}
module "redis" { # default v6.2; delete after v7.0 resource is bound
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1"
cf_org_name = local.cf_org_name
@@ -16,20 +16,6 @@ module "redis" { # default v6.2; delete after v7.0 resource is bound
redis_plan_name = "redis-dev"
}
module "redis-v70" {
source = "github.com/GSA-TTS/terraform-cloudgov//redis?ref=v1.0.0"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
name = "${local.app_name}-redis-v70-${local.env}"
redis_plan_name = "redis-dev"
json_params = jsonencode(
{
"engineVersion" : "7.0",
}
)
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"

View File

@@ -1,9 +1,9 @@
terraform {
required_version = "~> 1.7"
required_version = "~> 1.0"
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.53.1"
version = "0.53.0"
}
}

View File

@@ -1,9 +1,9 @@
terraform {
required_version = "~> 1.7"
required_version = "~> 1.0"
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.53.1"
version = "0.53.0"
}
}
}

View File

@@ -6,7 +6,7 @@ locals {
recursive_delete = false
}
module "redis" { # default v6.2; delete after v7.0 resource is bound
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1"
cf_org_name = local.cf_org_name
@@ -16,20 +16,6 @@ module "redis" { # default v6.2; delete after v7.0 resource is bound
redis_plan_name = "redis-3node-large"
}
module "redis-v70" {
source = "github.com/GSA-TTS/terraform-cloudgov//redis?ref=v1.0.0"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
name = "${local.app_name}-redis-v70-${local.env}"
redis_plan_name = "redis-dev"
json_params = jsonencode(
{
"engineVersion" : "7.0",
}
)
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"

View File

@@ -1,9 +1,9 @@
terraform {
required_version = "~> 1.7"
required_version = "~> 1.0"
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.53.1"
version = "0.53.0"
}
}

View File

@@ -6,7 +6,7 @@ locals {
recursive_delete = true
}
module "redis" { # default v6.2; delete after v7.0 resource is bound
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1"
cf_org_name = local.cf_org_name
@@ -16,20 +16,6 @@ module "redis" { # default v6.2; delete after v7.0 resource is bound
redis_plan_name = "redis-dev"
}
module "redis-v70" {
source = "github.com/GSA-TTS/terraform-cloudgov//redis?ref=v1.0.0"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
name = "${local.app_name}-redis-v70-${local.env}"
redis_plan_name = "redis-dev"
json_params = jsonencode(
{
"engineVersion" : "7.0",
}
)
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"

View File

@@ -1,9 +1,9 @@
terraform {
required_version = "~> 1.7"
required_version = "~> 1.0"
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.53.1"
version = "0.53.0"
}
}

View File

@@ -1,9 +1,9 @@
terraform {
required_version = "~> 1.7"
required_version = "~> 1.0"
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.53.1"
version = "0.53.0"
}
}
}

View File

@@ -6,7 +6,7 @@ locals {
recursive_delete = true
}
module "redis" { # default v6.2; delete after v7.0 resource is bound
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1"
cf_org_name = local.cf_org_name
@@ -16,20 +16,6 @@ module "redis" { # default v6.2; delete after v7.0 resource is bound
redis_plan_name = "redis-dev"
}
module "redis-v70" {
source = "github.com/GSA-TTS/terraform-cloudgov//redis?ref=v1.0.0"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
name = "${local.app_name}-redis-v70-${local.env}"
redis_plan_name = "redis-dev"
json_params = jsonencode(
{
"engineVersion" : "7.0",
}
)
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"

View File

@@ -1,9 +1,9 @@
terraform {
required_version = "~> 1.7"
required_version = "~> 1.0"
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.53.1"
version = "0.53.0"
}
}

View File

@@ -228,18 +228,12 @@ def test_can_show_notifications_if_data_retention_not_available(
url_for,
".download_notifications_csv",
message_type=None,
number_of_days="seven_day",
),
),
(
create_active_user_with_permissions(),
{"status": "failed"},
partial(
url_for,
".download_notifications_csv",
status="failed",
number_of_days="seven_day",
),
partial(url_for, ".download_notifications_csv", status="failed"),
),
(
create_active_user_with_permissions(),
@@ -248,13 +242,15 @@ def test_can_show_notifications_if_data_retention_not_available(
url_for,
".download_notifications_csv",
message_type="sms",
number_of_days="seven_day",
),
),
(
create_active_user_view_permissions(),
{},
partial(url_for, ".download_notifications_csv", number_of_days="seven_day"),
partial(
url_for,
".download_notifications_csv",
),
),
(
create_active_caseworking_user(),

Some files were not shown because too many files have changed in this diff Show More