Merge branch 'main' of https://github.com/cisagov/manage.get.gov into es/1975-fed-agency-update-script

This commit is contained in:
Rebecca Hsieh 2024-04-30 15:57:53 -07:00
commit e03fc95e44
No known key found for this signature in database
26 changed files with 697 additions and 173 deletions

View file

@ -22,9 +22,16 @@ jobs:
- name: Compile USWDS assets - name: Compile USWDS assets
working-directory: ./src working-directory: ./src
run: | run: |
docker compose run node npm install && docker compose run node bash -c "\
docker compose run node npx gulp copyAssets && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
docker compose run node npx gulp compile export NVM_DIR=\"\$HOME/.nvm\" && \
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
nvm install 21.7.3 && \
nvm use 21.7.3 && \
npm install && \
npx gulp copyAssets && \
npx gulp compile"
- name: Collect static assets - name: Collect static assets
working-directory: ./src working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input run: docker compose run app python manage.py collectstatic --no-input
@ -45,4 +52,4 @@ jobs:
cf_password: ${{ secrets.CF_DEVELOPMENT_PASSWORD }} cf_password: ${{ secrets.CF_DEVELOPMENT_PASSWORD }}
cf_org: cisa-dotgov cf_org: cisa-dotgov
cf_space: development cf_space: development
cf_command: "run-task getgov-development --command 'python manage.py migrate' --name migrate" cf_command: "run-task getgov-development --command 'python manage.py migrate' --name migrate"

View file

@ -42,9 +42,16 @@ jobs:
- name: Compile USWDS assets - name: Compile USWDS assets
working-directory: ./src working-directory: ./src
run: | run: |
docker compose run node npm install && docker compose run node bash -c "\
docker compose run node npx gulp copyAssets && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
docker compose run node npx gulp compile export NVM_DIR=\"\$HOME/.nvm\" && \
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
nvm install 21.7.3 && \
nvm use 21.7.3 && \
npm install && \
npx gulp copyAssets && \
npx gulp compile"
- name: Collect static assets - name: Collect static assets
working-directory: ./src working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input run: docker compose run app python manage.py collectstatic --no-input
@ -75,4 +82,4 @@ jobs:
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.' body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.'
}) })

View file

@ -23,9 +23,16 @@ jobs:
- name: Compile USWDS assets - name: Compile USWDS assets
working-directory: ./src working-directory: ./src
run: | run: |
docker compose run node npm install && docker compose run node bash -c "\
docker compose run node npx gulp copyAssets && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
docker compose run node npx gulp compile export NVM_DIR=\"\$HOME/.nvm\" && \
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
nvm install 21.7.3 && \
nvm use 21.7.3 && \
npm install && \
npx gulp copyAssets && \
npx gulp compile"
- name: Collect static assets - name: Collect static assets
working-directory: ./src working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input run: docker compose run app python manage.py collectstatic --no-input

View file

@ -23,9 +23,16 @@ jobs:
- name: Compile USWDS assets - name: Compile USWDS assets
working-directory: ./src working-directory: ./src
run: | run: |
docker compose run node npm install && docker compose run node bash -c "\
docker compose run node npx gulp copyAssets && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
docker compose run node npx gulp compile export NVM_DIR=\"\$HOME/.nvm\" && \
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" && \
[ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" && \
nvm install 21.7.3 && \
nvm use 21.7.3 && \
npm install && \
npx gulp copyAssets && \
npx gulp compile"
- name: Collect static assets - name: Collect static assets
working-directory: ./src working-directory: ./src
run: docker compose run app python manage.py collectstatic --no-input run: docker compose run app python manage.py collectstatic --no-input
@ -44,4 +51,4 @@ jobs:
cf_password: ${{ secrets.CF_STAGING_PASSWORD }} cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
cf_org: cisa-dotgov cf_org: cisa-dotgov
cf_space: staging cf_space: staging
cf_command: "run-task getgov-staging --command 'python manage.py migrate' --name migrate" cf_command: "run-task getgov-staging --command 'python manage.py migrate' --name migrate"

View file

@ -602,18 +602,18 @@ That data are synthesized from the generic_org_type field and the is_election_bo
The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view). The latest domain_election_board csv can be found [here](https://drive.google.com/file/d/1aDeCqwHmBnXBl2arvoFCN0INoZmsEGsQ/view).
After downloading this file, place it in `src/migrationdata` After downloading this file, place it in `src/migrationdata`
#### Step 2: Upload the domain_election_board file to your sandbox #### Step 3: Upload the domain_election_board file to your sandbox
Follow [Step 1: Transfer data to sandboxes](#step-1-transfer-data-to-sandboxes) and [Step 2: Transfer uploaded files to the getgov directory](#step-2-transfer-uploaded-files-to-the-getgov-directory) from the [Set Up Migrations on Sandbox](#set-up-migrations-on-sandbox) portion of this doc. Follow [Step 1: Transfer data to sandboxes](#step-1-transfer-data-to-sandboxes) and [Step 2: Transfer uploaded files to the getgov directory](#step-2-transfer-uploaded-files-to-the-getgov-directory) from the [Set Up Migrations on Sandbox](#set-up-migrations-on-sandbox) portion of this doc.
#### Step 2: SSH into your environment #### Step 4: SSH into your environment
```cf ssh getgov-{space}``` ```cf ssh getgov-{space}```
Example: `cf ssh getgov-za` Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance #### Step 5: Create a shell instance
```/tmp/lifecycle/shell``` ```/tmp/lifecycle/shell```
#### Step 4: Running the script #### Step 6: Running the script
```./manage.py populate_organization_type {domain_election_board_filename}``` ```./manage.py populate_organization_type {domain_election_board_filename}```
- The domain_election_board_filename file must adhere to this format: - The domain_election_board_filename file must adhere to this format:
@ -642,3 +642,29 @@ Example (assuming that this is being ran from src/):
| | Parameter | Description | | | Parameter | Description |
|:-:|:------------------------------------|:-------------------------------------------------------------------| |:-:|:------------------------------------|:-------------------------------------------------------------------|
| 1 | **domain_election_board_filename** | A file containing every domain that is an election office. | 1 | **domain_election_board_filename** | A file containing every domain that is an election office.
## Populate Verification Type
This section outlines how to run the `populate_verification_type` script.
The script is used to update the verification_type field on User when it is None.
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 4: Running the script
```./manage.py populate_verification_type```
### Running locally
#### Step 1: Running the script
```docker-compose exec app ./manage.py populate_verification_type```

View file

@ -9,7 +9,7 @@ cfenv = "*"
django-cors-headers = "*" django-cors-headers = "*"
pycryptodomex = "*" pycryptodomex = "*"
django-allow-cidr = "*" django-allow-cidr = "*"
django-auditlog = "2.3.0" django-auditlog = "*"
django-csp = "*" django-csp = "*"
environs = {extras=["django"]} environs = {extras=["django"]}
Faker = "*" Faker = "*"

218
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "ce10883aef7e1ce10421d99b3ac35ebf419857a3fe468f0e2d93785f4323eaa8" "sha256": "16a0db98015509322cf1d27f06fced5b7635057c4eb98921a9419d63d51925ab"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@ -32,20 +32,20 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87", "sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d",
"sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3" "sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.88" "version": "==1.34.90"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e", "sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133",
"sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b" "sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.88" "version": "==1.34.90"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
@ -313,12 +313,12 @@
}, },
"django-auditlog": { "django-auditlog": {
"hashes": [ "hashes": [
"sha256:7bc2c87e4aff62dec9785d1b2359a2b27148f8c286f8a52b9114fc7876c5a9f7", "sha256:92db1cf4a51ceca5c26b3ff46997d9e3305a02da1bd435e2efb5b8b6d300ce1f",
"sha256:b9d3acebb64f3f2785157efe3f2f802e0929aafc579d85bbfb9827db4adab532" "sha256:9de49f80a4911135d136017123cd73461f869b4947eec14d5e76db4b88182f3f"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.8'",
"version": "==2.3.0" "version": "==3.0.0"
}, },
"django-cache-url": { "django-cache-url": {
"hashes": [ "hashes": [
@ -958,96 +958,96 @@
}, },
"pydantic": { "pydantic": {
"hashes": [ "hashes": [
"sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352", "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5",
"sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383" "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.7.0" "version": "==2.7.1"
}, },
"pydantic-core": { "pydantic-core": {
"hashes": [ "hashes": [
"sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6", "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b",
"sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb", "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a",
"sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0", "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90",
"sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6", "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d",
"sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47", "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e",
"sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a", "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d",
"sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a", "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027",
"sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac", "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804",
"sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88", "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347",
"sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db", "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400",
"sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d", "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3",
"sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d", "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399",
"sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9", "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349",
"sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e", "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd",
"sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b", "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c",
"sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d", "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e",
"sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649", "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413",
"sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c", "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3",
"sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1", "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e",
"sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09", "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3",
"sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0", "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91",
"sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90", "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce",
"sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d", "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c",
"sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294", "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb",
"sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144", "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664",
"sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b", "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6",
"sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1", "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd",
"sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b", "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3",
"sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2", "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af",
"sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad", "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043",
"sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622", "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350",
"sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17", "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7",
"sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06", "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0",
"sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc", "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563",
"sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50", "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761",
"sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d", "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72",
"sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59", "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3",
"sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539", "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb",
"sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a", "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788",
"sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b", "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b",
"sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5", "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c",
"sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9", "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038",
"sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278", "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250",
"sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6", "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec",
"sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44", "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c",
"sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0", "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74",
"sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb", "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81",
"sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80", "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439",
"sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5", "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75",
"sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570", "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0",
"sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b", "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8",
"sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de", "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150",
"sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6", "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438",
"sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8", "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae",
"sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203", "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857",
"sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7", "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038",
"sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048", "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374",
"sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae", "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f",
"sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89", "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241",
"sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f", "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592",
"sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926", "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4",
"sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2", "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d",
"sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76", "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b",
"sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d", "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b",
"sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411", "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182",
"sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9", "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e",
"sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2", "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641",
"sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586", "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70",
"sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35", "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9",
"sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c", "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a",
"sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143", "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543",
"sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6", "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b",
"sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60", "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f",
"sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b", "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38",
"sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226", "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845",
"sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519", "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2",
"sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31", "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0",
"sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7", "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4",
"sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b" "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.18.1" "version": "==2.18.2"
}, },
"pydantic-settings": { "pydantic-settings": {
"hashes": [ "hashes": [
@ -1281,12 +1281,12 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:168894499578a9d69d6f7deb5811952bf4171c51b95749a9aef32cf67bc71f87", "sha256:2824e3dd18743ca50e5b10439d20e74647b1416e8a94509cb30beac92d27a18d",
"sha256:1bd4cef11b7c5f293cede50f3d33ca89fe3413c51f1864f40163c56a732dd6b3" "sha256:b2e5cb5b95efcc881e25a3bc872d7a24e75ff4e76f368138e4baf7b9d6ee3422"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.88" "version": "==1.34.90"
}, },
"boto3-mocking": { "boto3-mocking": {
"hashes": [ "hashes": [
@ -1299,28 +1299,28 @@
}, },
"boto3-stubs": { "boto3-stubs": {
"hashes": [ "hashes": [
"sha256:23ca9e0cd0d3e7702d6631a1e94a4208a26b39fa6b12c734427e68a7fa649477", "sha256:7361f162523168ddcfb3e0cc70e5208e78f95b9f1f2553032036a2b67ab33355",
"sha256:8f472d1bf09743c3d33304ecc8830d70ebe3ca19ac9604ae8da9af55421b0fce" "sha256:c82f3db8558e28f766361ba1eea7c77dff735f72fef2a0b9dffaa9c0d9ae76a3"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.88" "version": "==1.34.90"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:36f2e9e8dfa856e55dbbe703aea601f134db3fddc3615f1020a755b27fd26a5e", "sha256:113cd4c0cb63e13163ccbc2bb13d551be314ba7f8ba5bfab1c51a19ca01aa133",
"sha256:e87a660599ed3e14b2a770f4efc3df2f2f6d04f3c7bfd64ddbae186667864a7b" "sha256:d48f152498e2c60b43ce25b579d26642346a327b6fb2c632d57219e0a4f63392"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.34.88" "version": "==1.34.90"
}, },
"botocore-stubs": { "botocore-stubs": {
"hashes": [ "hashes": [
"sha256:656e966ea152a4f2828892aa7a9673bc91799998f5a8efd8e8fe390f61c2f4f1", "sha256:b2d7416b524bce7325aa5fe09bb5e0b6bc9531d4136f4407fa39b6bc58507f34",
"sha256:f55b03ae2e1706bd56299fd2975bb048f96aa49012a866e931a040a74f85c3cc" "sha256:d9b66542cbb8fbe28eef3c22caf941ae22d36cc1ef55b93fc0b52239457cab57"
], ],
"markers": "python_version >= '3.8' and python_version < '4.0'", "markers": "python_version >= '3.8' and python_version < '4.0'",
"version": "==1.34.88" "version": "==1.34.89"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@ -1497,11 +1497,11 @@
}, },
"platformdirs": { "platformdirs": {
"hashes": [ "hashes": [
"sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf",
"sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.2.0" "version": "==4.2.1"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [

View file

@ -4,8 +4,10 @@ from django.http import HttpResponse
from django.test import Client, TestCase, RequestFactory from django.test import Client, TestCase, RequestFactory
from django.urls import reverse from django.urls import reverse
from api.tests.common import less_console_noise_decorator
from djangooidc.exceptions import StateMismatch, InternalError from djangooidc.exceptions import StateMismatch, InternalError
from ..views import login_callback from ..views import login_callback
from registrar.models import User, Contact, VerifiedByStaff, DomainInvitation, TransitionDomain, Domain
from .common import less_console_noise from .common import less_console_noise
@ -16,6 +18,14 @@ class ViewsTest(TestCase):
self.client = Client() self.client = Client()
self.factory = RequestFactory() self.factory = RequestFactory()
def tearDown(self):
User.objects.all().delete()
Contact.objects.all().delete()
DomainInvitation.objects.all().delete()
VerifiedByStaff.objects.all().delete()
TransitionDomain.objects.all().delete()
Domain.objects.all().delete()
def say_hi(*args): def say_hi(*args):
return HttpResponse("Hi") return HttpResponse("Hi")
@ -229,6 +239,140 @@ class ViewsTest(TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/") self.assertEqual(response.url, "/")
@less_console_noise_decorator
def test_login_callback_sets_verification_type_regular(self, mock_client):
"""
Test that openid sets the verification type to regular on the returned user.
Regular, in this context, means that this user was "Verifed by Login.gov"
"""
# SETUP
session = self.client.session
session.save()
# MOCK
# mock that callback returns user_info; this is the expected behavior
mock_client.callback.side_effect = self.user_info
# patch that the request does not require step up auth
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
"djangooidc.views._initialize_client"
) as mock_init_client:
with patch("djangooidc.views._client_is_none", return_value=True):
# TEST
# test the login callback url
response = self.client.get(reverse("openid_login_callback"))
# assert that _initialize_client was called
mock_init_client.assert_called_once()
# Assert that we get a redirect
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
# Test the created user object
created_user = User.objects.get(email="test@example.com")
self.assertEqual(created_user.verification_type, User.VerificationTypeChoices.REGULAR)
@less_console_noise_decorator
def test_login_callback_sets_verification_type_invited(self, mock_client):
"""Test that openid sets the verification type to invited on the returned user
when they exist in the DomainInvitation table"""
# SETUP
session = self.client.session
session.save()
domain, _ = Domain.objects.get_or_create(name="test123.gov")
invitation, _ = DomainInvitation.objects.get_or_create(email="test@example.com", domain=domain)
# MOCK
# mock that callback returns user_info; this is the expected behavior
mock_client.callback.side_effect = self.user_info
# patch that the request does not require step up auth
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
"djangooidc.views._initialize_client"
) as mock_init_client:
with patch("djangooidc.views._client_is_none", return_value=True):
# TEST
# test the login callback url
response = self.client.get(reverse("openid_login_callback"))
# assert that _initialize_client was called
mock_init_client.assert_called_once()
# Assert that we get a redirect
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
# Test the created user object
created_user = User.objects.get(email="test@example.com")
self.assertEqual(created_user.email, invitation.email)
self.assertEqual(created_user.verification_type, User.VerificationTypeChoices.INVITED)
@less_console_noise_decorator
def test_login_callback_sets_verification_type_grandfathered(self, mock_client):
"""Test that openid sets the verification type to grandfathered
on a user which exists in our TransitionDomain table"""
# SETUP
session = self.client.session
session.save()
# MOCK
# mock that callback returns user_info; this is the expected behavior
mock_client.callback.side_effect = self.user_info
td, _ = TransitionDomain.objects.get_or_create(username="test@example.com", domain_name="test123.gov")
# patch that the request does not require step up auth
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
"djangooidc.views._initialize_client"
) as mock_init_client:
with patch("djangooidc.views._client_is_none", return_value=True):
# TEST
# test the login callback url
response = self.client.get(reverse("openid_login_callback"))
# assert that _initialize_client was called
mock_init_client.assert_called_once()
# Assert that we get a redirect
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
# Test the created user object
created_user = User.objects.get(email="test@example.com")
self.assertEqual(created_user.email, td.username)
self.assertEqual(created_user.verification_type, User.VerificationTypeChoices.GRANDFATHERED)
@less_console_noise_decorator
def test_login_callback_sets_verification_type_verified_by_staff(self, mock_client):
"""Test that openid sets the verification type to verified_by_staff
on a user which exists in our VerifiedByStaff table"""
# SETUP
session = self.client.session
session.save()
# MOCK
# mock that callback returns user_info; this is the expected behavior
mock_client.callback.side_effect = self.user_info
vip, _ = VerifiedByStaff.objects.get_or_create(email="test@example.com")
# patch that the request does not require step up auth
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
"djangooidc.views._initialize_client"
) as mock_init_client:
with patch("djangooidc.views._client_is_none", return_value=True):
# TEST
# test the login callback url
response = self.client.get(reverse("openid_login_callback"))
# assert that _initialize_client was called
mock_init_client.assert_called_once()
# Assert that we get a redirect
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
# Test the created user object
created_user = User.objects.get(email="test@example.com")
self.assertEqual(created_user.email, vip.email)
self.assertEqual(created_user.verification_type, User.VerificationTypeChoices.VERIFIED_BY_STAFF)
def test_login_callback_no_step_up_auth(self, mock_client): def test_login_callback_no_step_up_auth(self, mock_client):
"""Walk through login_callback when _requires_step_up_auth returns False """Walk through login_callback when _requires_step_up_auth returns False
and assert that we have a redirect to /""" and assert that we have a redirect to /"""

View file

@ -99,8 +99,22 @@ def login_callback(request):
return CLIENT.create_authn_request(request.session) return CLIENT.create_authn_request(request.session)
user = authenticate(request=request, **userinfo) user = authenticate(request=request, **userinfo)
if user: if user:
# Fixture users kind of exist in a superposition of verification types,
# because while the system "verified" them, if they login,
# we don't know how the user themselves was verified through login.gov until
# they actually try logging in. This edge-case only matters in non-production environments.
fixture_user = User.VerificationTypeChoices.FIXTURE_USER
is_fixture_user = user.verification_type and user.verification_type == fixture_user
# Set the verification type if it doesn't already exist or if its a fixture user
if not user.verification_type or is_fixture_user:
user.set_user_verification_type()
user.save()
login(request, user) login(request, user)
logger.info("Successfully logged in user %s" % user) logger.info("Successfully logged in user %s" % user)
# Clear the flag if the exception is not caught # Clear the flag if the exception is not caught
request.session.pop("redirect_attempted", None) request.session.pop("redirect_attempted", None)
return redirect(request.session.get("next", "/")) return redirect(request.session.get("next", "/"))

View file

@ -1,5 +1,5 @@
FROM docker.io/cimg/node:current-browsers FROM docker.io/cimg/node:current-browsers
FROM node:21.7.3
WORKDIR /app WORKDIR /app
# Install app dependencies # Install app dependencies
@ -7,4 +7,6 @@ WORKDIR /app
# where available (npm@5+) # where available (npm@5+)
COPY --chown=circleci:circleci package*.json ./ COPY --chown=circleci:circleci package*.json ./
RUN npm install
RUN npm install -g npm@10.5.0
RUN npm install

4
src/package-lock.json generated
View file

@ -15,6 +15,10 @@
}, },
"devDependencies": { "devDependencies": {
"@uswds/compile": "^1.0.0-beta.3" "@uswds/compile": "^1.0.0-beta.3"
},
"engines": {
"node": "21.7.3",
"npm": "10.5.0"
} }
}, },
"node_modules/@gulp-sourcemaps/identity-map": { "node_modules/@gulp-sourcemaps/identity-map": {

View file

@ -3,6 +3,11 @@
"version": "1.0.0", "version": "1.0.0",
"description": "========================", "description": "========================",
"main": "index.js", "main": "index.js",
"engines": {
"node": "21.7.3",
"npm": "10.5.0"
},
"engineStrict": true,
"scripts": { "scripts": {
"pa11y-ci": "pa11y-ci", "pa11y-ci": "pa11y-ci",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -17,4 +22,4 @@
"devDependencies": { "devDependencies": {
"@uswds/compile": "^1.0.0-beta.3" "@uswds/compile": "^1.0.0-beta.3"
} }
} }

View file

@ -537,7 +537,7 @@ class MyUserAdmin(BaseUserAdmin):
fieldsets = ( fieldsets = (
( (
None, None,
{"fields": ("username", "password", "status")}, {"fields": ("username", "password", "status", "verification_type")},
), ),
("Personal Info", {"fields": ("first_name", "last_name", "email")}), ("Personal Info", {"fields": ("first_name", "last_name", "email")}),
( (
@ -555,13 +555,20 @@ class MyUserAdmin(BaseUserAdmin):
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
) )
readonly_fields = ("verification_type",)
# Hide Username (uuid), Groups and Permissions # Hide Username (uuid), Groups and Permissions
# Q: Now that we're using Groups and Permissions, # Q: Now that we're using Groups and Permissions,
# do we expose those to analysts to view? # do we expose those to analysts to view?
analyst_fieldsets = ( analyst_fieldsets = (
( (
None, None,
{"fields": ("status",)}, {
"fields": (
"status",
"verification_type",
)
},
), ),
("Personal Info", {"fields": ("first_name", "last_name", "email")}), ("Personal Info", {"fields": ("first_name", "last_name", "email")}),
( (
@ -681,11 +688,14 @@ class MyUserAdmin(BaseUserAdmin):
return [] return []
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
readonly_fields = list(self.readonly_fields)
if request.user.has_perm("registrar.full_access_permission"): if request.user.has_perm("registrar.full_access_permission"):
return () # No read-only fields for all access users return readonly_fields
# Return restrictive Read-only fields for analysts and else:
# users who might not belong to groups # Return restrictive Read-only fields for analysts and
return self.analyst_readonly_fields # users who might not belong to groups
return self.analyst_readonly_fields
class HostIPInline(admin.StackedInline): class HostIPInline(admin.StackedInline):

View file

@ -630,3 +630,8 @@ address.dja-address-contact-list {
.usa-button__small-text { .usa-button__small-text {
font-size: small; font-size: small;
} }
// Get rid of padding on all help texts
form .aligned p.help, form .aligned div.help {
padding-left: 0px !important;
}

View file

@ -780,3 +780,11 @@ if DEBUG:
# due to Docker, bypass Debug Toolbar's check on INTERNAL_IPS # due to Docker, bypass Debug Toolbar's check on INTERNAL_IPS
"SHOW_TOOLBAR_CALLBACK": lambda _: True, "SHOW_TOOLBAR_CALLBACK": lambda _: True,
} }
# From https://django-auditlog.readthedocs.io/en/latest/upgrade.html
# Run:
# cf run-task getgov-<> --wait --command 'python manage.py auditlogmigratejson --traceback' --name auditlogmigratejson
# on our staging and stable, then remove these 2 variables or set to False
AUDITLOG_TWO_STEP_MIGRATION = True
AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT = True

View file

@ -7,6 +7,7 @@ from registrar.models import (
UserGroup, UserGroup,
) )
fake = Faker() fake = Faker()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -207,6 +208,10 @@ class UserFixture:
user.email = user_data["email"] user.email = user_data["email"]
user.is_staff = True user.is_staff = True
user.is_active = True user.is_active = True
# This verification type will get reverted to "regular" (or whichever is applicables)
# once the user logs in for the first time (as they then got verified through different means).
# In the meantime, we can still describe how the user got here in the first place.
user.verification_type = User.VerificationTypeChoices.FIXTURE_USER
group = UserGroup.objects.get(name=group_name) group = UserGroup.objects.get(name=group_name)
user.groups.add(group) user.groups.add(group)
user.save() user.save()

View file

@ -0,0 +1,23 @@
import logging
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
from registrar.models import User
logger = logging.getLogger(__name__)
class Command(BaseCommand, PopulateScriptTemplate):
help = "Loops through each valid User object and updates its verification_type value"
def handle(self, **kwargs):
"""Loops through each valid User object and updates its verification_type value"""
filter_condition = {"verification_type__isnull": True}
self.mass_populate_field(User, filter_condition, ["verification_type"])
def populate_field(self, field_to_update):
"""Defines how we update the verification_type field"""
field_to_update.set_user_verification_type()
logger.info(
f"{TerminalColors.OKCYAN}Updating {field_to_update} => "
f"{field_to_update.verification_type}{TerminalColors.OKCYAN}"
)

View file

@ -1,5 +1,6 @@
import logging import logging
import sys import sys
from abc import ABC, abstractmethod
from django.core.paginator import Paginator from django.core.paginator import Paginator
from typing import List from typing import List
from registrar.utility.enums import LogCode from registrar.utility.enums import LogCode
@ -58,6 +59,55 @@ class ScriptDataHelper:
model_class.objects.bulk_update(page.object_list, fields_to_update) model_class.objects.bulk_update(page.object_list, fields_to_update)
class PopulateScriptTemplate(ABC):
"""
Contains an ABC for generic populate scripts
"""
def mass_populate_field(self, sender, filter_conditions, fields_to_update):
"""Loops through each valid "sender" object - specified by filter_conditions - and
updates fields defined by fields_to_update using populate_function.
You must define populate_field before you can use this function.
"""
objects = sender.objects.filter(**filter_conditions)
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
info_to_inspect=f"""
==Proposed Changes==
Number of {sender} objects to change: {len(objects)}
These fields will be updated on each record: {fields_to_update}
""",
prompt_title="Do you wish to patch this data?",
)
logger.info("Updating...")
to_update: List[sender] = []
failed_to_update: List[sender] = []
for updated_object in objects:
try:
self.populate_field(updated_object)
to_update.append(updated_object)
except Exception as err:
failed_to_update.append(updated_object)
logger.error(err)
logger.error(f"{TerminalColors.FAIL}" f"Failed to update {updated_object}" f"{TerminalColors.ENDC}")
# Do a bulk update on the first_ready field
ScriptDataHelper.bulk_update_fields(sender, to_update, fields_to_update)
# Log what happened
TerminalHelper.log_script_run_summary(to_update, failed_to_update, skipped=[], debug=True)
@abstractmethod
def populate_field(self, field_to_update):
"""Defines how we update each field. Must be defined before using mass_populate_field."""
raise NotImplementedError
class TerminalHelper: class TerminalHelper:
@staticmethod @staticmethod
def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool, log_header=None): def log_script_run_summary(to_update, failed_to_update, skipped, debug: bool, log_header=None):

View file

@ -0,0 +1,29 @@
# Generated by Django 4.2.10 on 2024-04-26 14:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0088_domaininformation_cisa_representative_email_and_more"),
]
operations = [
migrations.AddField(
model_name="user",
name="verification_type",
field=models.CharField(
blank=True,
choices=[
("grandfathered", "Legacy user"),
("verified_by_staff", "Verified by staff"),
("regular", "Verified by Login.gov"),
("invited", "Invited by a domain manager"),
("fixture_user", "Created by fixtures"),
],
help_text="The means through which this user was verified",
null=True,
),
),
]

View file

@ -2,6 +2,7 @@ import logging
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.db.models import Q
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
@ -23,6 +24,28 @@ class User(AbstractUser):
but can be customized later. but can be customized later.
""" """
class VerificationTypeChoices(models.TextChoices):
"""
Users achieve access to our system in a few different ways.
These choices reflect those pathways.
Overview of verification types:
- GRANDFATHERED: User exists in the `TransitionDomain` table
- VERIFIED_BY_STAFF: User exists in the `VerifiedByStaff` table
- INVITED: User exists in the `DomainInvitation` table
- REGULAR: User was verified through IAL2
- FIXTURE_USER: User was created by fixtures
"""
GRANDFATHERED = "grandfathered", "Legacy user"
VERIFIED_BY_STAFF = "verified_by_staff", "Verified by staff"
REGULAR = "regular", "Verified by Login.gov"
INVITED = "invited", "Invited by a domain manager"
# We need a type for fixture users (rather than using verified by staff)
# because those users still do get "verified" through normal means
# after they login.
FIXTURE_USER = "fixture_user", "Created by fixtures"
# #### Constants for choice fields #### # #### Constants for choice fields ####
RESTRICTED = "restricted" RESTRICTED = "restricted"
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),) STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
@ -50,6 +73,13 @@ class User(AbstractUser):
db_index=True, db_index=True,
) )
verification_type = models.CharField(
choices=VerificationTypeChoices.choices,
null=True,
blank=True,
help_text="The means through which this user was verified",
)
def __str__(self): def __str__(self):
# this info is pulled from Login.gov # this info is pulled from Login.gov
if self.first_name or self.last_name: if self.first_name or self.last_name:
@ -114,23 +144,61 @@ class User(AbstractUser):
except Exception as err: except Exception as err:
raise err raise err
# A new incoming user who is a domain manager for one of the domains # We can't set the verification type here because the user may not
# that we inputted from Verisign (that is, their email address appears # always exist at this point. We do it down the line.
# in the username field of a TransitionDomain) verification_type = cls.get_verification_type_from_email(email)
if TransitionDomain.objects.filter(username=email).exists():
return False
# New users flagged by Staff to bypass ial2 # Checks if the user needs verification.
if VerifiedByStaff.objects.filter(email=email).exists(): # The user needs identity verification if they don't meet
return False # any special criteria, i.e. we are validating them "regularly"
return verification_type == cls.VerificationTypeChoices.REGULAR
# A new incoming user who is being invited to be a domain manager (that is, def set_user_verification_type(self):
# their email address is in DomainInvitation for an invitation that is not yet "retrieved"). """
invited = DomainInvitation.DomainInvitationStatus.INVITED Given pre-existing data from TransitionDomain, VerifiedByStaff, and DomainInvitation,
if DomainInvitation.objects.filter(email=email, status=invited).exists(): set the verification "type" defined in VerificationTypeChoices.
return False """
email_or_username = self.email if self.email else self.username
retrieved = DomainInvitation.DomainInvitationStatus.RETRIEVED
verification_type = self.get_verification_type_from_email(email_or_username, invitation_status=retrieved)
return True # An existing user may have been invited to a domain after they got verified.
# We need to check for this condition.
if verification_type == User.VerificationTypeChoices.INVITED:
invitation = (
DomainInvitation.objects.filter(email=email_or_username, status=retrieved)
.order_by("created_at")
.first()
)
# If you joined BEFORE the oldest invitation was created, then you were verified normally.
# (See logic in get_verification_type_from_email)
if not invitation and self.date_joined < invitation.created_at:
verification_type = User.VerificationTypeChoices.REGULAR
self.verification_type = verification_type
@classmethod
def get_verification_type_from_email(cls, email, invitation_status=DomainInvitation.DomainInvitationStatus.INVITED):
"""Retrieves the verification type based off of a provided email address"""
verification_type = None
if TransitionDomain.objects.filter(Q(username=email) | Q(email=email)).exists():
# A new incoming user who is a domain manager for one of the domains
# that we inputted from Verisign (that is, their email address appears
# in the username field of a TransitionDomain)
verification_type = cls.VerificationTypeChoices.GRANDFATHERED
elif VerifiedByStaff.objects.filter(email=email).exists():
# New users flagged by Staff to bypass ial2
verification_type = cls.VerificationTypeChoices.VERIFIED_BY_STAFF
elif DomainInvitation.objects.filter(email=email, status=invitation_status).exists():
# A new incoming user who is being invited to be a domain manager (that is,
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
verification_type = cls.VerificationTypeChoices.INVITED
else:
verification_type = cls.VerificationTypeChoices.REGULAR
return verification_type
def check_domain_invitations_on_login(self): def check_domain_invitations_on_login(self):
"""When a user first arrives on the site, we need to retrieve any domain """When a user first arrives on the site, we need to retrieve any domain

View file

@ -3,7 +3,7 @@
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} {% if user.has_contact_info %}margin-bottom-1{% endif %} dja-address-contact-list"> <address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} {% if user.has_contact_info %}margin-bottom-1{% endif %} dja-address-contact-list">
{% if show_formatted_name %} {% if show_formatted_name %}
{% if contact.get_formatted_name %} {% if user.get_formatted_name %}
<a href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a><br /> <a href="{% url 'admin:registrar_contact_change' user.id %}">{{ user.get_formatted_name }}</a><br />
{% else %} {% else %}
None<br /> None<br />
@ -47,7 +47,12 @@
{% else %} {% else %}
None<br> None<br>
{% endif %} {% endif %}
{% else %} {% else %}
No additional contact information found. No additional contact information found.<br>
{% endif %}
{% if user_verification_type %}
{{ user_verification_type }}
{% endif %} {% endif %}
</address> </address>

View file

@ -4,6 +4,7 @@
{% comment %} {% comment %}
This is using a custom implementation fieldset.html (see admin/fieldset.html) This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endcomment %} {% endcomment %}
{% block field_readonly %} {% block field_readonly %}
{% with all_contacts=original_object.other_contacts.all %} {% with all_contacts=original_object.other_contacts.all %}
{% if field.field.name == "other_contacts" %} {% if field.field.name == "other_contacts" %}
@ -65,22 +66,15 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endwith %} {% endwith %}
{% endblock field_readonly %} {% endblock field_readonly %}
{% block help_text %}
<div class="help margin-bottom-1" {% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
<div>{{ field.field.help_text|safe }}</div>
</div>
{% endblock help_text %}
{% block after_help_text %} {% block after_help_text %}
{% if field.field.name == "creator" %} {% if field.field.name == "creator" %}
<div class="flex-container tablet:margin-top-1"> <div class="flex-container tablet:margin-top-2">
<label aria-label="Creator contact details"></label> <label aria-label="Creator contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %} {% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%}
</div> </div>
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %} {% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
{% elif field.field.name == "submitter" %} {% elif field.field.name == "submitter" %}
<div class="flex-container"> <div class="flex-container tablet:margin-top-2">
<label aria-label="Submitter contact details"></label> <label aria-label="Submitter contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %} {% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
</div> </div>

View file

@ -3065,7 +3065,15 @@ class TestMyUserAdmin(TestCase):
request.user = create_user() request.user = create_user()
fieldsets = self.admin.get_fieldsets(request) fieldsets = self.admin.get_fieldsets(request)
expected_fieldsets = ( expected_fieldsets = (
(None, {"fields": ("status",)}), (
None,
{
"fields": (
"status",
"verification_type",
)
},
),
("Personal Info", {"fields": ("first_name", "last_name", "email")}), ("Personal Info", {"fields": ("first_name", "last_name", "email")}),
("Permissions", {"fields": ("is_active", "groups")}), ("Permissions", {"fields": ("is_active", "groups")}),
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),

View file

@ -14,8 +14,9 @@ from registrar.models import (
TransitionDomain, TransitionDomain,
DomainInformation, DomainInformation,
UserDomainRole, UserDomainRole,
VerifiedByStaff,
PublicContact,
) )
from registrar.models.public_contact import PublicContact
from django.core.management import call_command from django.core.management import call_command
from unittest.mock import patch, call from unittest.mock import patch, call
@ -25,6 +26,103 @@ from .common import MockEppLib, less_console_noise, completed_domain_request
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
class TestPopulateVerificationType(MockEppLib):
"""Tests for the populate_organization_type script"""
def setUp(self):
"""Creates a fake domain object"""
super().setUp()
# Get the domain requests
self.domain_request_1 = completed_domain_request(
name="lasers.gov",
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
is_election_board=True,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
# Approve the request
self.domain_request_1.approve()
# Get the domains
self.domain_1 = Domain.objects.get(name="lasers.gov")
# Get users
self.regular_user, _ = User.objects.get_or_create(username="testuser@igormail.gov")
vip, _ = VerifiedByStaff.objects.get_or_create(email="vipuser@igormail.gov")
self.verified_by_staff_user, _ = User.objects.get_or_create(username="vipuser@igormail.gov")
grandfathered, _ = TransitionDomain.objects.get_or_create(
username="grandpa@igormail.gov", domain_name=self.domain_1.name
)
self.grandfathered_user, _ = User.objects.get_or_create(username="grandpa@igormail.gov")
invited, _ = DomainInvitation.objects.get_or_create(
email="invited@igormail.gov", domain=self.domain_1, status=DomainInvitation.DomainInvitationStatus.RETRIEVED
)
self.invited_user, _ = User.objects.get_or_create(username="invited@igormail.gov")
self.untouched_user, _ = User.objects.get_or_create(
username="iaminvincible@igormail.gov", verification_type=User.VerificationTypeChoices.GRANDFATHERED
)
# Fixture users should be untouched by the script. These will auto update once the
# user logs in / creates an account.
self.fixture_user, _ = User.objects.get_or_create(
username="fixture@igormail.gov", verification_type=User.VerificationTypeChoices.FIXTURE_USER
)
def tearDown(self):
"""Deletes all DB objects related to migrations"""
super().tearDown()
# Delete domains and related information
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
User.objects.all().delete()
Contact.objects.all().delete()
Website.objects.all().delete()
@less_console_noise_decorator
def run_populate_verification_type(self):
"""
This method executes the populate_organization_type command.
The 'call_command' function from Django's management framework is then used to
execute the populate_organization_type command with the specified arguments.
"""
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("populate_verification_type")
@less_console_noise_decorator
def test_verification_type_script_populates_data(self):
"""Ensures that the verification type script actually populates data"""
# Run the script
self.run_populate_verification_type()
# Scripts don't work as we'd expect in our test environment, we need to manually
# trigger the refresh event
self.regular_user.refresh_from_db()
self.grandfathered_user.refresh_from_db()
self.invited_user.refresh_from_db()
self.verified_by_staff_user.refresh_from_db()
self.untouched_user.refresh_from_db()
# Test all users
self.assertEqual(self.regular_user.verification_type, User.VerificationTypeChoices.REGULAR)
self.assertEqual(self.grandfathered_user.verification_type, User.VerificationTypeChoices.GRANDFATHERED)
self.assertEqual(self.invited_user.verification_type, User.VerificationTypeChoices.INVITED)
self.assertEqual(self.verified_by_staff_user.verification_type, User.VerificationTypeChoices.VERIFIED_BY_STAFF)
self.assertEqual(self.untouched_user.verification_type, User.VerificationTypeChoices.GRANDFATHERED)
self.assertEqual(self.fixture_user.verification_type, User.VerificationTypeChoices.FIXTURE_USER)
class TestPopulateOrganizationType(MockEppLib): class TestPopulateOrganizationType(MockEppLib):
"""Tests for the populate_organization_type script""" """Tests for the populate_organization_type script"""

View file

@ -344,8 +344,6 @@ class TestDomainManagers(TestDomainOverview):
def tearDown(self): def tearDown(self):
"""Ensure that the user has its original permissions""" """Ensure that the user has its original permissions"""
super().tearDown() super().tearDown()
self.user.is_staff = False
self.user.save()
def test_domain_managers(self): def test_domain_managers(self):
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))

View file

@ -1,8 +1,8 @@
-i https://pypi.python.org/simple -i https://pypi.python.org/simple
annotated-types==0.6.0; python_version >= '3.8' annotated-types==0.6.0; python_version >= '3.8'
asgiref==3.8.1; python_version >= '3.8' asgiref==3.8.1; python_version >= '3.8'
boto3==1.34.88; python_version >= '3.8' boto3==1.34.90; python_version >= '3.8'
botocore==1.34.88; python_version >= '3.8' botocore==1.34.90; python_version >= '3.8'
cachetools==5.3.3; python_version >= '3.7' cachetools==5.3.3; python_version >= '3.7'
certifi==2024.2.2; python_version >= '3.6' certifi==2024.2.2; python_version >= '3.6'
cfenv==0.5.3 cfenv==0.5.3
@ -15,7 +15,7 @@ dj-email-url==1.0.6
django==4.2.10; python_version >= '3.8' django==4.2.10; python_version >= '3.8'
django-admin-multiple-choice-list-filter==0.1.1 django-admin-multiple-choice-list-filter==0.1.1
django-allow-cidr==0.7.1 django-allow-cidr==0.7.1
django-auditlog==2.3.0; python_version >= '3.7' django-auditlog==3.0.0; python_version >= '3.8'
django-cache-url==3.4.5 django-cache-url==3.4.5
django-cors-headers==4.3.1; python_version >= '3.8' django-cors-headers==4.3.1; python_version >= '3.8'
django-csp==3.8 django-csp==3.8
@ -44,8 +44,8 @@ phonenumberslite==8.13.35
psycopg2-binary==2.9.9; python_version >= '3.7' psycopg2-binary==2.9.9; python_version >= '3.7'
pycparser==2.22; python_version >= '3.8' pycparser==2.22; python_version >= '3.8'
pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pydantic==2.7.0; python_version >= '3.8' pydantic==2.7.1; python_version >= '3.8'
pydantic-core==2.18.1; python_version >= '3.8' pydantic-core==2.18.2; python_version >= '3.8'
pydantic-settings==2.2.1; python_version >= '3.8' pydantic-settings==2.2.1; python_version >= '3.8'
pyjwkest==1.4.2 pyjwkest==1.4.2
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'