Merge pull request #42 from cisagov/sspj/proof-of-concept

Add Django
This commit is contained in:
Seamus Johnston 2022-08-19 13:24:28 +00:00 committed by GitHub
commit 5e4c44a444
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 951 additions and 15 deletions

42
.github/workflows/deploy.yaml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Build and deploy
# This workflow runs on pushes to main (typically,
# a merged pull request) and on pushes of tagged commits.
# Pushes to main will deploy to Unstable; tagged commits
# will deploy to Staging
on:
push:
branches:
- main
tags:
- v*
workflow_dispatch:
jobs:
deploy-unstable:
# if this job runs on a branch, we deduce that code
# has been pushed to main and should be deployed to unstable
if: ${{ github.ref_type == 'branch' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to cloud.gov sandbox
uses: 18f/cg-deploy-action@main
env:
DEPLOY_NOW: thanks
with:
cf_username: ${{ secrets.CF_USERNAME }}
cf_password: ${{ secrets.CF_PASSWORD }}
cf_org: sandbox-gsa
cf_space: dotgov-poc
push_arguments: "-f ops/manifests/manifest-unstable.yaml"
# deploy-staging:
# # if this job runs on a tag, we deduce that code
# # has been tagged for release and should be deployed to staging
# if: ${{ github.ref_type == 'tag' }}

34
.github/workflows/migrate.yaml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Run Migrations
# This workflow can be run from the CLI
# gh workflow run migrate.yaml -f environment=sandbox
# OR
# cf run-task getgov-unstable --wait \
# --command 'python manage.py migrate' --name migrate
on:
workflow_dispatch:
inputs:
environment:
type: choice
description: Where should we run migrations
options:
- unstable
- staging
jobs:
migrate-unstable:
if: ${{ github.event.inputs.environment == 'unstable' }}
runs-on: ubuntu-latest
steps:
- name: Run Django migrations for unstable
uses: 18f/cg-deploy-action@main
with:
cf_username: ${{ secrets.CF_USERNAME }}
cf_password: ${{ secrets.CF_PASSWORD }}
cf_org: sandbox-gsa
cf_space: dotgov-poc
full_command: "cf run-task getgov-unstable --wait --command 'python manage.py migrate' --name migrate"
# migrate:
# if: ${{ github.event.inputs.environment == 'staging' }}

151
.gitignore vendored
View file

@ -1 +1,150 @@
docs/research/data/**
### Project specific ###
docs/research/data/**
static/
credentials*
### The usual garbage files ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# .python-version
# pipenv
#Pipfile.lock
# poetry
#poetry.lock
# pdm
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
.idea/
# Node
node_modules

View file

@ -0,0 +1,30 @@
# 7. Python Buildpack
Date: 2022-08-12
## Status
Accepted
## Context
We had previously drafted ADRs to use Docker to build images for containerized deployment. The motivation was to reduce dependency on Cloud.gov. These ADRs were rejected, mainly owing to the necessity of having an image repository to hold the built images. The team had concerns about an overreliance on GitHub with their GitHub Packages service and about the overall maintenance burden of any image repository.
Cloud.gov uses Cloud Foundry which provides several “buildpacks”. These are automated environments which will take a code repository of a certain language and do the usual setup steps to prepare a deployment of that code. In the case of Python, this means automated detection of Pipfile and installation of packages.
We do not anticipate needing a custom buildpack, because our current use case falls completely within the Python buildpack's purview.
## Decision
To use Cloud Foundrys Python buildpack.
## Consequences
There will be a small amount of future work if the code is migrated off of Cloud.gov, but only a small amount. Proportionate to the overall effort of migration, it is inconsequential.
Cloud.gov provides [documentation around the trade-offs](https://cloud.gov/docs/deployment/docker/):
| | Supported buildpack | Docker container |
|---|---|---|
|Pros|It “just works”. Automatic and constant security updates. All you need to do is write code.|Can build container images and run containers on local workstation. Fine-grained control over compilation and root filesystem.|
|Cons|Difficult to recreate the execution environment locally. Testing compilation and the result of staging is harder.|Added responsibility for all security updates and bug fixes. More compliance responsibility means more work.|

View file

@ -0,0 +1,25 @@
# 8. Server Side Rendering
Date: 2022-08-16
## Status
Accepted
## Context
In deciding how content would be delivered to the end user, we held a meeting with Engineering and Design ~and the mayor of Igorville~ [^1] and consulted the [18F Engineering Practices Guide](https://engineering.18f.gov/web-architecture/).
The two major options considered were 1) a single page application, developed with a JavaScript framework such as React, perhaps served from its own Node-based server or a static file host such as Federalist; or 2) traditional server rendered HTML pages, delivered by Django, accompanied by a small amount of JavaScript and CSS.
## Decision
To use server-side rendering performed by Django.
## Consequences
The positive aspects of this include: easier state management, fewer bugs caused by stale cache data, easier 508 compliance for disabled users, better maintainability for future developers due to fewer dependencies and a shallower learning curve, and better cross browser compatibility.
The risks to this approach are given by the frontend lead on a sister 18F project: a less well-developed API since this approach does not require an API upfront, users/stakeholders attempting to request ever increasing levels of interactivity, and familiarity with vanilla.js has waned over the years.
[^1]: Inside joke. To obtain access to the registrant flow, a member of our team signed up for .gov using the fictitious town of Igorville.

View file

@ -0,0 +1,41 @@
digraph sequenceDiagram {
# Install graphviz and run `fdp -Tpng request.dot -o request_diagram.png`
subgraph cluster_1 {
label="Request and Response";
browserHead [ label="{Browser|user makes request}" pos="0.1,4.75!" shape="record" ];
browserPoint0 [ pos="0.1,4!" shape="point" width="0" ]
browserPoint5 [ pos="0.1,0.75!" shape="point" width="0" ]
browserFoot [ label="Browser" pos="0,0!" shape="record" ];
viewHead [ label="{Django Views|business logic applied}" pos="2.5,4.75!" shape="record" ];
viewPoint0 [ pos="2.5,4!" shape="point" width="0" ]
viewPoint1 [ pos="2.5,3.75!" shape="point" width="0" ]
viewPoint2 [ pos="2.5,3.5!" shape="point" width="0" ]
viewPoint3 [ pos="2.5,1.25!" shape="point" width="0" ]
viewPoint4 [ pos="2.5,1!" shape="point" width="0" ]
viewPoint5 [ pos="2.5,0.75!" shape="point" width="0" ]
viewFoot [ label="Django view" pos="2.5,0!" shape="record" ];
databaseHead [ label="{Django ORM|database consulted}" pos="5,4.75!" shape="record" ];
databasePoint1 [ pos="5,3.75!" shape="point" width="0" ]
databasePoint2 [ pos="5,3.5!" shape="point" width="0" ]
databaseFoot [ label="Django ORM" pos="5,2.5!" shape="record" ];
templateHead [ label="{Django Templates|html response prepared}" pos="5,2.25!" shape="record" ];
templatePoint3 [ pos="5,1.25!" shape="point" width="0" ]
templatePoint4 [ pos="5,1!" shape="point" width="0" ]
templateFoot [ label="Django Templates" pos="5,0!" shape="record" ];
browserHead -> browserPoint0 -> browserFoot [ dir="none" style="dashed" ]
viewHead -> viewPoint0 -> viewFoot [ dir="none" style="dashed" ]
databaseHead -> databasePoint1 -> databaseFoot [ dir="none" style="dashed" ]
templateHead -> templatePoint3 -> templateFoot [ dir="none" style="dashed" ]
browserPoint0 -> viewPoint0 [ style="solid" ]
viewPoint1 -> databasePoint1 [ style="solid" ]
databasePoint2 -> viewPoint2 [ style="solid" ]
viewPoint3 -> templatePoint3 [ style="solid" ]
templatePoint4 -> viewPoint4 [ style="solid" ]
viewPoint5 -> browserPoint5 [ style="solid" ]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -0,0 +1,22 @@
digraph systemDiagram {
# Install graphviz and run `fdp -Tpng system.dot -o system_diagram.png`
subgraph cluster_0 {
label="System Diagram";
node [shape=record];
registrant [label="Registrant"];
subgraph cluster_cloud {
label="cloud.gov";
node [shape=record];
postgres [label="Postgres Database"];
subgraph cluster_django {
label="Django MVC";
node [shape=record];
models [pos="0,1!" label="Database ORM"];
views [pos="1,.5!" label="Views"];
templates [pos="1,0!" label="Templates"];
}
}
registrant -> views [dir=both];
models -> postgres [dir=both];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

59
docs/ops/README.md Normal file
View file

@ -0,0 +1,59 @@
# Operations
========================
## Authenticating
You'll need the [Cloud Foundry CLI](https://docs.cloud.gov/getting-started/setup/).
We use the V7 Cloud Foundry CLI.
```shell
cf login -a api.fr.cloud.gov --sso
```
After authenticating, make sure you are targeting the correct org and space!
```bash
cf spaces
cf target -o <ORG> -s <SPACE>
```
## Rotating Environment Secrets
Secrets were originally created with:
```sh
cf cups getgov-credentials -p credentials-<ENVIRONMENT>.json
```
Where `credentials-<ENVIRONMENT>.json` looks like:
```json
{
"DJANGO_SECRET_KEY": "EXAMPLE",
...
}
```
You can see the current environment with `cf env <APP>`, for example `cf env getgov-unstable`.
The command `cups` stands for [create user provided service](https://docs.cloudfoundry.org/devguide/services/user-provided.html). User provided services are the way currently recommended by Cloud.gov for deploying secrets. The user provided service is bound to the application in `manifest-<ENVIRONMENT>.json`.
To rotate secrets, create a new `credentials-<ENVIRONMENT>.json` file, upload it, then restage the app.
Example:
```bash
cf uups getgov-credentials -p credentials-unstable.json
cf restage getgov-unstable --strategy rolling
```
Non-secret environment variables can be declared in `manifest-<ENVIRONMENT>.json` directly.
## Database
In sandbox, created with `cf create-service aws-rds micro-psql getgov-database`.
Binding the database in `manifest-<ENVIRONMENT>.json` automatically inserts the connection string into the environment as `DATABASE_URL`.
[Cloud.gov RDS documentation](https://cloud.gov/docs/services/relational-database/).

6
ops/README.md Normal file
View file

@ -0,0 +1,6 @@
# Operations
========================
This directory contains files related to deploying or running the application(s).
Documentation is in [docs/ops](../docs/ops).

4
ops/manifests/README.md Normal file
View file

@ -0,0 +1,4 @@
# Manifests
========================
This directory contains files used by the deployment pipeline to deploy to Cloud.gov.

View file

@ -0,0 +1,23 @@
---
applications:
- name: getgov-unstable
buildpacks:
- python_buildpack
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs3
timeout: 180
command: gunicorn registrar.config.wsgi -t 60
health-check-type: http
health-check-http-endpoint: /health
env:
# Send stdout and stderr straight to the terminal without buffering
PYTHONUNBUFFERED: yup
# Tell Django where to find its configuration
DJANGO_SETTINGS_MODULE: registrar.config.settings
routes:
- route: getgov-unstable.app.cloud.gov
services:
- getgov-credentials
- getgov-database

View file

@ -5,4 +5,12 @@ name = "pypi"
[packages]
django = "*"
cfenv = "*"
django-allow-cidr = "*"
django-csp = "*"
environs = {extras=["django"]}
gunicorn = "*"
psycopg2-binary = "*"
[dev-packages]
django-debug-toolbar = "*"

225
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "8b5635a4f7b069ae6661115b9eaa15466f7cd96794af5d131735a3638be101fb"
"sha256": "3c3bdeb59b98dcd8c01350c36619ab12b593f8c25846dc3425f481b587fc0ae8"
},
"pipfile-spec": 6,
"requires": {},
@ -22,6 +22,28 @@
"markers": "python_version >= '3.7'",
"version": "==3.5.2"
},
"cfenv": {
"hashes": [
"sha256:7815bffcc4a3db350f92517157fafc577c11b5a7ff172dc5632f1042b93073e8",
"sha256:c7a91a4c82431acfc35db664c194d5e6cc7f4df3dcb692d0f836a6ceb0156167"
],
"index": "pypi",
"version": "==0.5.3"
},
"dj-database-url": {
"hashes": [
"sha256:ccf3e8718f75ddd147a1e212fca88eecdaa721759ee48e38b485481c77bca3dc",
"sha256:cd354a3b7a9136d78d64c17b2aec369e2ae5616fbca6bfbe435ef15bb372ce39"
],
"version": "==1.0.0"
},
"dj-email-url": {
"hashes": [
"sha256:64257c4f9d8139a4af8e5267229d32260e433fbf257b0cf8fc855bb0cc39ca7d",
"sha256:ef36f8a324ec57cf3be5c7a7ef44ed6900ca0208624a918ab33adc1cf6427b39"
],
"version": "==1.0.5"
},
"django": {
"hashes": [
"sha256:031ccb717782f6af83a0063a1957686e87cb4581ea61b47b3e9addf60687989a",
@ -30,6 +52,172 @@
"index": "pypi",
"version": "==4.1"
},
"django-allow-cidr": {
"hashes": [
"sha256:2fd88ffe697caf0c1d0fd147b88cf44d81282c069bbc475166a2ff1637ad9155",
"sha256:d17347e75d6c02864022f52ed608775a5e9ab144d1a82bb40853714f125f5d87"
],
"index": "pypi",
"version": "==0.5.0"
},
"django-cache-url": {
"hashes": [
"sha256:6cc9901a99a99751f5458aa7de08ce06e48c1441b1a94c9457d78af74fab9a26",
"sha256:c4a62634cffc9d636073cef597a44576d67b07660ab2ef1f02b160ee7ecf0e98"
],
"version": "==3.4.2"
},
"django-csp": {
"hashes": [
"sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a",
"sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"
],
"index": "pypi",
"version": "==3.7"
},
"environs": {
"extras": [
"django"
],
"hashes": [
"sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124",
"sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"
],
"index": "pypi",
"version": "==9.5.0"
},
"furl": {
"hashes": [
"sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e",
"sha256:9ab425062c4217f9802508e45feb4a83e54324273ac4b202f1850363309666c0"
],
"version": "==2.1.3"
},
"gunicorn": {
"hashes": [
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
],
"index": "pypi",
"version": "==20.1.0"
},
"marshmallow": {
"hashes": [
"sha256:00040ab5ea0c608e8787137627a8efae97fabd60552a05dc889c888f814e75eb",
"sha256:635fb65a3285a31a30f276f30e958070f5214c7196202caa5c7ecf28f5274bc7"
],
"markers": "python_version >= '3.7'",
"version": "==3.17.0"
},
"orderedmultidict": {
"hashes": [
"sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad",
"sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"
],
"version": "==1.0.1"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"psycopg2-binary": {
"hashes": [
"sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7",
"sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76",
"sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa",
"sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9",
"sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004",
"sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1",
"sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094",
"sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57",
"sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af",
"sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554",
"sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232",
"sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c",
"sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b",
"sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834",
"sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2",
"sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71",
"sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460",
"sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e",
"sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4",
"sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d",
"sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d",
"sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9",
"sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f",
"sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063",
"sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478",
"sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092",
"sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c",
"sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce",
"sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1",
"sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65",
"sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e",
"sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4",
"sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029",
"sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33",
"sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39",
"sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53",
"sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307",
"sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42",
"sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35",
"sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8",
"sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb",
"sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae",
"sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e",
"sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f",
"sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba",
"sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24",
"sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca",
"sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb",
"sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef",
"sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42",
"sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1",
"sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667",
"sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272",
"sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281",
"sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e",
"sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"
],
"index": "pypi",
"version": "==2.9.3"
},
"pyparsing": {
"hashes": [
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.9"
},
"python-dotenv": {
"hashes": [
"sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f",
"sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"
],
"markers": "python_version >= '3.5'",
"version": "==0.20.0"
},
"setuptools": {
"hashes": [
"sha256:7a2e7e95c3bf33f356b4c59aee7a6848585c4219dd3e941e43cc117888f210e4",
"sha256:c04a012ae3a1b2cc2aeed4893377b70ea61c6c143d0acceea16ec4b60de6e40d"
],
"markers": "python_version >= '3.7'",
"version": "==65.0.1"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlparse": {
"hashes": [
"sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",
@ -39,5 +227,38 @@
"version": "==0.4.2"
}
},
"develop": {}
"develop": {
"asgiref": {
"hashes": [
"sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4",
"sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"
],
"markers": "python_version >= '3.7'",
"version": "==3.5.2"
},
"django": {
"hashes": [
"sha256:031ccb717782f6af83a0063a1957686e87cb4581ea61b47b3e9addf60687989a",
"sha256:032f8a6fc7cf05ccd1214e4a2e21dfcd6a23b9d575c6573cacc8c67828dbe642"
],
"index": "pypi",
"version": "==4.1"
},
"django-debug-toolbar": {
"hashes": [
"sha256:89a52128309eb4da12738801ff0c202d2ff8730d1c3225fac6acf630c303e661",
"sha256:97965f2630692de316ea0c1ca5bfa81660d7ba13146dbc6be2059cf55b35d0e5"
],
"index": "pypi",
"version": "==3.5.0"
},
"sqlparse": {
"hashes": [
"sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",
"sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.2"
}
}
}

View file

@ -11,6 +11,7 @@ If you're new to Django, see [Getting Started with Django](https://www.djangopro
* Initialize the application:
```shell
cd src
docker-compose build
```
* Run the server: `docker-compose up`
@ -18,4 +19,4 @@ If you're new to Django, see [Getting Started with Django](https://www.djangopro
### Update Dependencies
1. Check the [Pipfile](./src/Pipfile) for pinned dependencies and manually adjust the version numbers
1. Run `docker-compose up` and `docker-compose exec app pipenv update` to perform the upgrade and generate a new [Pipfile.lock](./src/Pipfile.lock)
1. Run `cd src`, `docker-compose up`, and `docker-compose exec app pipenv update` to perform the upgrade and generate a new [Pipfile.lock](./src/Pipfile.lock)

View file

@ -2,8 +2,10 @@ version: "3.0"
services:
app:
build: .
depends_on:
- db
volumes:
- ./app:/app
- .:/app
links:
- db
working_dir: /app
@ -13,19 +15,22 @@ services:
condition: on-failure
max_attempts: 5
environment:
# Ensure stdout and stderr are sent straight to the terminal without buffering
# Send stdout and stderr straight to the terminal without buffering
- PYTHONUNBUFFERED=yup
# In case we'd like to know
- RUNNING_IN_DOCKER=yup
# How to connect to Postgre container
- DATABASE_URL=postgres://user:feedabee@db/app
# Run in development mode on our local
- DJANGO_SETTINGS_MODULE=app.settings.dev
# Tell Django where to find its configuration
- DJANGO_SETTINGS_MODULE=registrar.config.settings
# Set a local key for Django
- DJANGO_SECRET_KEY=feedabee
# Run Django in debug mode on local
- DJANGO_DEBUG=True
stdin_open: true
tty: true
ports:
- "8000:8000"
command: "python"
- "8080:8000"
# command: "python"
command: "python manage.py runserver 0.0.0.0:8000"
db:
image: postgres:latest

View file

@ -11,7 +11,7 @@ container are also owned by the same user on the host system.
import sys
import os
import pwd
import subprocess # nosec
import subprocess
HOST_UID = os.stat("/app").st_uid
HOST_USER = "james"
@ -41,7 +41,7 @@ if __name__ == "__main__":
username += "0"
home_dir = "/home/%s" % username
subprocess.check_call(
[ # nosec
[
"useradd",
"-d",
home_dir,
@ -53,4 +53,4 @@ if __name__ == "__main__":
)
os.environ["HOME"] = "/home/%s" % pwd.getpwuid(HOST_UID).pw_name
os.setuid(HOST_UID)
os.execvp(sys.argv[1], sys.argv[1:]) # nosec
os.execvp(sys.argv[1], sys.argv[1:])

21
src/manage.py Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View file

View file

View file

@ -0,0 +1,203 @@
"""
Django settings for .gov registrar project.
Generated by 'django-admin startproject' using Django 4.0.6.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
IF you'd like to see all of these settings in the running app:
```shell
$ docker-compose exec app python manage.py shell
>>> from django.conf import settings
>>> dir(settings)
```
"""
import environs
import os
from cfenv import AppEnv
from pathlib import Path
env = environs.Env()
# Get secrets from Cloud.gov user provided service, if exists
# If not, get secrets from environment variables
key_service = AppEnv().get_service(name="getgov-credentials")
if key_service and key_service.credentials:
secret = key_service.credentials.get
else:
secret = env
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = secret("DJANGO_SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool("DJANGO_DEBUG", default=False)
# TODO: configure and document security settings
ALLOWED_HOSTS = [
'getgov-unstable.app.cloud.gov',
'get.gov'
]
ALLOWED_CIDR_NETS = ['10.0.0.0/8'] # nosec
USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CORS_ALLOW_ALL_ORIGINS = False
# TODO: are all of these needed? Need others?
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
# TODO: document these for future maintainers
MIDDLEWARE = [
'allow_cidr.middleware.AllowCIDRMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'csp.middleware.CSPMiddleware',
]
# TODO: decide on template engine and document in ADR
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# No file logger is configured, because containerized apps
# do not log to the file system.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] "
"%(message)s",
"datefmt": "%d/%b/%Y %H:%M:%S",
},
"simple": {
"format": "%(levelname)s %(message)s",
},
},
"handlers": {
"console": {
"level": "DEBUG" if DEBUG else "INFO",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"loggers": {
"django": {
"handlers": ["console"],
"propagate": True,
"level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"),
},
"django.template": {
"handlers": ["console"],
"propagate": True,
"level": "INFO",
},
"registrar": {
"handlers": ["console"],
"propagate": True,
"level": "INFO",
},
},
}
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': env.dj_db_url('DATABASE_URL'),
}
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
USE_L10N = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_ROOT = BASE_DIR / 'static'
STATIC_URL = 'static/'
ADMIN_URL = 'admin/'
ROOT_URLCONF = 'registrar.config.urls'
WSGI_APPLICATION = 'registrar.config.wsgi.application'
# TODO: FAC example for REST framework
API_VERSION = "0"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.BasicAuthentication",
"users.auth.ExpiringTokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
"TEST_REQUEST_RENDERER_CLASSES": [
"rest_framework.renderers.MultiPartRenderer",
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.TemplateHTMLRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
],
"TEST_REQUEST_DEFAULT_FORMAT": "api",
}
# TODO: FAC example for login.gov
SIMPLE_JWT = {
'ALGORITHM': 'RS256',
'AUDIENCE': None,
'ISSUER': 'https://idp.int.identitysandbox.gov/',
'JWK_URL': 'https://idp.int.identitysandbox.gov/api/openid_connect/certs',
'LEEWAY': 0,
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.UntypedToken',),
'USER_ID_CLAIM': 'sub',
}
TOKEN_AUTH = {"TOKEN_TTL": 3600}
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View file

@ -0,0 +1,23 @@
"""URL Configuration
For more information see:
https://docs.djangoproject.com/en/4.0/topics/http/urls/
"""
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from registrar.views import health
urlpatterns = [
path("admin/", admin.site.urls),
path("health/", health.health)
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns

View file

@ -0,0 +1,14 @@
"""
WSGI config.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

View file

View file

@ -0,0 +1,4 @@
from django.http import HttpResponse
def health(request):
return HttpResponse("OK")

1
src/runtime.txt Normal file
View file

@ -0,0 +1 @@
3.10.x