Merge pull request #134 from dod-ccpo/monolith

Monolith
This commit is contained in:
richard-dds 2018-08-07 11:36:24 -04:00 committed by GitHub
commit 667a2cb5b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
236 changed files with 12903 additions and 2293 deletions

7
.gitignore vendored
View File

@ -23,7 +23,14 @@ static/fonts
# font files originate from a node module
static/fonts/*
static/assets
static/assets/*
# local log files
log/*
config/dev.ini
# CRLs
/crl
/crl-tmp

View File

@ -1,7 +1,8 @@
sudo: required
language: python
python: "3.6"
services: docker
services:
- docker
git:
submodules: false
env:
@ -10,17 +11,22 @@ env:
- PROD_IMAGE_NAME=atst-prod
before_install:
# Use sed to replace the SSH URL with the public URL
- sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules
# Manually initialize submodules
- git submodule update --init --recursive
# Use sed to replace the SSH URL with the public URL
- sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules
# Manually initialize submodules
- git submodule update --init --recursive
before_script:
- docker run -d --name postgres96 postgres:9.6-alpine
- docker run -d --name redis redis:4.0.10-alpine
- docker run --link postgres96:postgres96 --link redis:redis waisbrot/wait
- export postgres_ip="$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" postgres96)"
- export redis_ip="$(docker inspect -f "{{ .NetworkSettings.IPAddress }}" redis)"
- docker login -u $ATAT_DOCKER_REGISTRY_USERNAME -p $ATAT_DOCKER_REGISTRY_PASSWORD $ATAT_DOCKER_REGISTRY_URL
- docker build --tag "${TESTER_IMAGE_NAME}" . -f deploy/docker/tester/Dockerfile
- docker build --tag "${TESTER_IMAGE_NAME}" --add-host "postgreshost:${postgres_ip}" --add-host "redishost:${redis_ip}" . -f deploy/docker/tester/Dockerfile
script:
- docker run "${TESTER_IMAGE_NAME}"
- docker run --add-host "postgreshost:${postgres_ip}" --add-host "redishost:${redis_ip}" "${TESTER_IMAGE_NAME}"
before_deploy:
- docker build --tag "${PROD_IMAGE_NAME}" . -f deploy/docker/prod/Dockerfile

21
Pipfile
View File

@ -4,22 +4,31 @@ verify_ssl = true
name = "pypi"
[packages]
tornado = "==5.0.2"
webassets = "==0.12.1"
Unipath = "==1.1"
wtforms-tornado = "*"
webassets = "*"
Unipath = "*"
pendulum = "*"
redis = "*"
sqlalchemy = "*"
alembic = "*"
"psycopg2-binary" = "*"
flask = "*"
flask-sqlalchemy = "*"
flask-assets = "*"
flask-session = "*"
flask-wtf = "*"
pyopenssl = "*"
requests = "*"
[dev-packages]
bandit = "*"
pytest = "==3.6.0"
pytest-tornado = "==0.5.0"
pytest = "*"
ipython = "*"
ipdb = "*"
pylint = "*"
black = "*"
pytest-watch = "*"
factory-boy = "*"
pytest-flask = "*"
[requires]
python_version = "3.6"

464
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "3bea02ccdb0e3877f2595d7fb405408114ec8947e0484d5b4aaf14a4c8ff78b2"
"sha256": "2b149e0d8c23814a2c701b53f5c75b36714a2ccd4e2a2769924ef6e2a3f09e97"
},
"pipfile-spec": 6,
"requires": {
@ -16,21 +16,242 @@
]
},
"default": {
"pendulum": {
"alembic": {
"hashes": [
"sha256:0643d45824e6789b88187728337dfa6075a0233f6976c2abefba00d064156309",
"sha256:3cc271195d8054bec06f54ff7d56ea6c2e2b5ad5dd6b532d787b34d2cabe6a65",
"sha256:544e44d8a92954e5ef4db4fa8b662d3282f2ac7b7c2cbf4227dc193ba78b9e1e",
"sha256:846478ab5f7480b3d850a09e44fe03830d448633c84f0b1066615ff6c34293aa",
"sha256:8bb523f759daeecfc0649369f198cbeb27a6608347354f4f847d21d579003db6",
"sha256:a449142063100f1b3c1119453c7569667c9ba79897305a1c50ca83a8c790f1e4",
"sha256:b7ff156b3d7cccbdeeb63465578d9a4e6f57d463f6ff6d4474254208d08f8353",
"sha256:d8822a592bbc16576c44ec4625bff9187ed9b649d47714e4905a55adc5b25339",
"sha256:dd45c7b349faab69714df9835cdf8bf8bce50bf6fc471419d3b23ba33e1915a5",
"sha256:fac088b637b5db5a047a0e89194d8c3c9e9e9ce1665089240003bb7c05b92536"
"sha256:52d73b1d750f1414fa90c25a08da47b87de1e4ad883935718a8f36396e19e78e",
"sha256:eb7db9b4510562ec37c91d00b00d95fde076c1030d3f661aea882eec532b3565"
],
"index": "pypi",
"version": "==2.0.2"
"version": "==1.0.0"
},
"asn1crypto": {
"hashes": [
"sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
"sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
],
"version": "==0.24.0"
},
"certifi": {
"hashes": [
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
],
"version": "==2018.4.16"
},
"cffi": {
"hashes": [
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"version": "==1.11.5"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
],
"version": "==6.7"
},
"cryptography": {
"hashes": [
"sha256:21af753934f2f6d1a10fe8f4c0a64315af209ef6adeaee63ca349797d747d687",
"sha256:27bb401a20a838d6d0ea380f08c6ead3ccd8c9d8a0232dc9adcc0e4994576a66",
"sha256:29720c4253263cff9aea64585adbbe85013ba647f6e98367efff9db2d7193ded",
"sha256:2a35b7570d8f247889784010aac8b384fd2e4a47b33e15c4a60b45a7c1944120",
"sha256:42c531a6a354407f42ee07fda5c2c0dc822cf6d52744949c182f2b295fbd4183",
"sha256:5eb86f03f9c4f0ac2336ac5431271072ddf7ecc76b338e26366732cfac58aa19",
"sha256:67f7f57eae8dede577f3f7775957f5bec93edd6bdb6ce597bb5b28e1bdf3d4fb",
"sha256:6ec84edcbc966ae460560a51a90046503ff0b5b66157a9efc61515c68059f6c8",
"sha256:7ba834564daef87557e7fcd35c3c3183a4147b0b3a57314e53317360b9b201b3",
"sha256:7d7f084cbe1fdb82be5a0545062b59b1ad3637bc5a48612ac2eb428ff31b31ea",
"sha256:82409f5150e529d699e5c33fa8fd85e965104db03bc564f5f4b6a9199e591f7c",
"sha256:87d092a7c2a44e5f7414ab02fb4145723ebba411425e1a99773531dd4c0e9b8d",
"sha256:8c56ef989342e42b9fcaba7c74b446f0cc9bed546dd00034fa7ad66fc00307ef",
"sha256:9449f5d4d7c516a6118fa9210c4a00f34384cb1d2028672100ee0c6cce49d7f6",
"sha256:bc2301170986ad82d9349a91eb8884e0e191209c45f5541b16aa7c0cfb135978",
"sha256:c132bab45d4bd0fff1d3fe294d92b0a6eb8404e93337b3127bdec9f21de117e6",
"sha256:c3d945b7b577f07a477700f618f46cbc287af3a9222cd73035c6ef527ef2c363",
"sha256:cee18beb4c807b5c0b178f4fa2fae03cef9d51821a358c6890f8b23465b7e5d2",
"sha256:d01dfc5c2b3495184f683574e03c70022674ca9a7be88589c5aba130d835ea90"
],
"version": "==2.3"
},
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
],
"index": "pypi",
"version": "==1.0.2"
},
"flask-assets": {
"hashes": [
"sha256:6031527b89fb3509d1581d932affa5a79dd348cfffb58d0aef99a43461d47847"
],
"index": "pypi",
"version": "==0.12"
},
"flask-session": {
"hashes": [
"sha256:a31c27e0c3287f00c825b3d9625aba585f4df4cccedb1e7dd5a69a215881a731",
"sha256:b9b32126bfc52c3169089f2ed9a40e34b589527bda48b633428e07d39d9c8792"
],
"index": "pypi",
"version": "==0.3.1"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b",
"sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53"
],
"index": "pypi",
"version": "==2.3.2"
},
"flask-wtf": {
"hashes": [
"sha256:5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36",
"sha256:d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac"
],
"index": "pypi",
"version": "==0.14.2"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"mako": {
"hashes": [
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
],
"version": "==1.0.7"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"pendulum": {
"hashes": [
"sha256:0ec5371949e147753661e1e98721273170638034dfceb578f29d69d93d3d474b",
"sha256:10ccdc8c6d004ba97883dd0f57503963ddf6cb83e849a16c4675ba18da657564",
"sha256:37bb54bcbb9d7fccd725f3fda69702e51ab3de9971b4c1c986505fbb3bc58bed",
"sha256:51803352e40778f914ff7af3494788b404260b415d9a9d607a8cf73e5e120994",
"sha256:5de295ca85761d9adf4020e6f3bed6eb933846ccf23b74e04b071f6d677f11a4",
"sha256:73f850265adcf0986fcc0af83ae9c8c5a7ca3c4a2525184110478a8bfd1a77b3",
"sha256:8fe289356322f6b0f4510082b4c412a1496a64054a37ae86b24411868a1901c6",
"sha256:c0401482dfa9fbd7005f2dfbf54ec61fd2c8130df37651ac2a3722d1f049ae4e",
"sha256:c358ee65ddb99c2b1bf301458e43ed09ff6d40465bcc9928265246912fad4d0f",
"sha256:d07962450e808556b3e6209a5830e2bbf8c7747129580c3b5b09e641f72617ab",
"sha256:dc05e6186c9c3b9969326aded9cba7a796744918581b25457f5148a5e3475d55",
"sha256:ee9466eea403e8e308c284d3055e285b97905a5ffb1566df0ef200b4f39c0f15",
"sha256:f7fa6220251a636112721e8158b9dd59018d818ec121047900934d80864eca62"
],
"index": "pypi",
"version": "==2.0.3"
},
"psycopg2-binary": {
"hashes": [
"sha256:04afb59bbbd2eab3148e6816beddc74348078b8c02a1113ea7f7822f5be4afe3",
"sha256:098b18f4d8857a8f9b206d1dc54db56c2255d5d26458917e7bcad61ebfe4338f",
"sha256:0bf855d4a7083e20ead961fda4923887094eaeace0ab2d76eb4aa300f4bbf5bd",
"sha256:197dda3ffd02057820be83fe4d84529ea70bf39a9a4daee1d20ffc74eb3d042e",
"sha256:278ef63afb4b3d842b4609f2c05ffbfb76795cf6a184deeb8707cd5ed3c981a5",
"sha256:3cbf8c4fc8f22f0817220891cf405831559f4d4c12c4f73913730a2ea6c47a47",
"sha256:4305aed922c4d9d6163ab3a41d80b5a1cfab54917467da8168552c42cad84d32",
"sha256:47ee296f704fb8b2a616dec691cdcfd5fa0f11943955e88faa98cbd1dc3b3e3d",
"sha256:4a0e38cb30457e70580903367161173d4a7d1381eb2f2cfe4e69b7806623f484",
"sha256:4d6c294c6638a71cafb82a37f182f24321f1163b08b5d5ca076e11fe838a3086",
"sha256:4f3233c366500730f839f92833194fd8f9a5c4529c8cd8040aa162c3740de8e5",
"sha256:5221f5a3f4ca2ddf0d58e8b8a32ca50948be9a43351fda797eb4e72d7a7aa34d",
"sha256:5c6ca0b507540a11eaf9e77dee4f07c131c2ec80ca0cffa146671bf690bc1c02",
"sha256:789bd89d71d704db2b3d5e67d6d518b158985d791d3b2dec5ab85457cfc9677b",
"sha256:7b94d29239efeaa6a967f3b5971bd0518d2a24edd1511edbf4a2c8b815220d07",
"sha256:89bc65ef3301c74cf32db25334421ea6adbe8f65601ea45dcaaf095abed910bb",
"sha256:89d6d3a549f405c20c9ae4dc94d7ed2de2fa77427a470674490a622070732e62",
"sha256:97521704ac7127d7d8ba22877da3c7bf4a40366587d238ec679ff38e33177498",
"sha256:a395b62d5f44ff6f633231abe568e2203b8fabf9797cd6386aa92497df912d9a",
"sha256:a6d32c37f714c3f34158f3fa659f3a8f2658d5f53c4297d45579b9677cc4d852",
"sha256:a89ee5c26f72f2d0d74b991ce49e42ddeb4ac0dc2d8c06a0f2770a1ab48f4fe0",
"sha256:b4c8b0ef3608e59317bfc501df84a61e48b5445d45f24d0391a24802de5f2d84",
"sha256:b5fcf07140219a1f71e18486b8dc28e2e1b76a441c19374805c617aa6d9a9d55",
"sha256:b86f527f00956ecebad6ab3bb30e3a75fedf1160a8716978dd8ce7adddedd86f",
"sha256:be4c4aa22ba22f70de36c98b06480e2f1697972d49eb20d525f400d204a6d272",
"sha256:c2ac7aa1a144d4e0e613ac7286dae85671e99fe7a1353954d4905629c36b811c",
"sha256:de26ef4787b5e778e8223913a3e50368b44e7480f83c76df1f51d23bd21cea16",
"sha256:e70ebcfc5372dc7b699c0110454fc4263967f30c55454397e5769eb72c0eb0ce",
"sha256:eadbd32b6bc48b67b0457fccc94c86f7ccc8178ab839f684eb285bb592dc143e",
"sha256:ecbc6dfff6db06b8b72ae8a2f25ff20fbdcb83cb543811a08f7cb555042aa729"
],
"index": "pypi",
"version": "==2.7.5"
},
"pycparser": {
"hashes": [
"sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226"
],
"version": "==2.18"
},
"pyopenssl": {
"hashes": [
"sha256:26ff56a6b5ecaf3a2a59f132681e2a80afcc76b4f902f612f518f92c2a1bf854",
"sha256:6488f1423b00f73b7ad5167885312bb0ce410d3312eb212393795b53c8caa580"
],
"index": "pypi",
"version": "==18.0.0"
},
"python-dateutil": {
"hashes": [
@ -39,12 +260,17 @@
],
"version": "==2.7.3"
},
"python-editor": {
"hashes": [
"sha256:a3c066acee22a1c94f63938341d4fb374e3fdd69366ed6603d7b24bed1efc565"
],
"version": "==1.0.3"
},
"pytzdata": {
"hashes": [
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
],
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*'",
"version": "==2018.5"
},
"redis": {
@ -55,6 +281,14 @@
"index": "pypi",
"version": "==2.10.6"
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
"index": "pypi",
"version": "==2.19.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
@ -62,16 +296,12 @@
],
"version": "==1.11.0"
},
"tornado": {
"sqlalchemy": {
"hashes": [
"sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7",
"sha256:408d129e9d13d3c55aa73f8084aa97d5f90ed84132e38d6932e63a67d5bec563",
"sha256:88ce0282cce70df9045e515f578c78f1ebc35dcabe1d70f800c3583ebda7f5f5",
"sha256:ba9fbb249ac5390bff8a1d6aa4b844fd400701069bda7d2e380dfe2217895101",
"sha256:c050089173c2e9272244bccfb6a8615fb9e53b79420a5551acfa76094ecc3111"
"sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8"
],
"index": "pypi",
"version": "==5.0.2"
"version": "==1.2.10"
},
"unipath": {
"hashes": [
@ -81,6 +311,14 @@
"index": "pypi",
"version": "==1.1"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
"markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version < '4'",
"version": "==1.23"
},
"webassets": {
"hashes": [
"sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db"
@ -88,19 +326,19 @@
"index": "pypi",
"version": "==0.12.1"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
},
"wtforms": {
"hashes": [
"sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61",
"sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1"
],
"version": "==2.2.1"
},
"wtforms-tornado": {
"hashes": [
"sha256:dadb5e504d01f14bf75900f592888bb402ada6b8f8235fe583359f562d351a3a"
],
"index": "pypi",
"version": "==0.0.2"
}
},
"develop": {
@ -128,10 +366,10 @@
},
"astroid": {
"hashes": [
"sha256:a8d8c7fe34e34e868426b9bafce852c355a3951eef60bc831b2ed541558f8d37",
"sha256:e722228b5259ce8c7cbf75f3b0ee8b483cfbd4df01167474a84087d1aeade22c"
"sha256:a48b57ede295c3188ef5c84273bc2a8eadc46e4cbb001eae0d49fb5d1fabbb19",
"sha256:d066cdeec5faeb51a4be5010da612680653d844b57afd86a5c8315f2f801b4cc"
],
"version": "==2.0.0.dev4"
"version": "==2.0.2"
},
"atomicwrites": {
"hashes": [
@ -197,19 +435,43 @@
],
"version": "==0.6.2"
},
"factory-boy": {
"hashes": [
"sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca",
"sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f"
],
"index": "pypi",
"version": "==2.11.1"
},
"faker": {
"hashes": [
"sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867",
"sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d"
],
"markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*'",
"version": "==0.8.17"
},
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
],
"index": "pypi",
"version": "==1.0.2"
},
"gitdb2": {
"hashes": [
"sha256:b60e29d4533e5e25bb50b7678bbc187c8f6bcff1344b4f293b2ba55c85795f09",
"sha256:cf9a4b68e8c4da8d42e48728c944ff7af2d8c9db303ac1ab32eac37aa4194b0e"
"sha256:87783b7f4a8f6b71c7fe81d32179b3c8781c1a7d6fa0c69bff2f315b00aff4f8",
"sha256:bb4c85b8a58531c51373c89f92163b92f30f81369605a67cd52d1fc21246c044"
],
"version": "==2.0.3"
"version": "==2.0.4"
},
"gitpython": {
"hashes": [
"sha256:1ec4c44846cf76a1e55769560673a97731849c9d05401e035e607495f10db959",
"sha256:b60b045cf64a321e5b620debb49890099fa6c7be6dfb7fb249027e5d34227301"
"sha256:563221e5a44369c6b79172f455584c9ebbb122a13368cc82cb4b5addff788f82",
"sha256:8237dc5bfd6f1366abeee5624111b9d6879393d84745a507de0fda86043b65a8"
],
"version": "==2.1.10"
"version": "==2.1.11"
},
"ipdb": {
"hashes": [
@ -220,11 +482,11 @@
},
"ipython": {
"hashes": [
"sha256:a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f",
"sha256:eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c"
"sha256:007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62",
"sha256:b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4"
],
"index": "pypi",
"version": "==6.4.0"
"version": "==6.5.0"
},
"ipython-genutils": {
"hashes": [
@ -239,9 +501,14 @@
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
],
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*'",
"version": "==4.3.4"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jedi": {
"hashes": [
"sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1",
@ -249,6 +516,13 @@
],
"version": "==0.12.1"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"lazy-object-proxy": {
"hashes": [
"sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
@ -283,6 +557,12 @@
],
"version": "==1.3.1"
},
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
@ -292,11 +572,11 @@
},
"more-itertools": {
"hashes": [
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8",
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3",
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0"
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
],
"version": "==4.2.0"
"version": "==4.3.0"
},
"parso": {
"hashes": [
@ -313,10 +593,10 @@
},
"pbr": {
"hashes": [
"sha256:4f2b11d95917af76e936811be8361b2b19616e5ef3b55956a429ec7864378e0c",
"sha256:e0f23b61ec42473723b2fec2f33fb12558ff221ee551962f01dd4de9053c2055"
"sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45",
"sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa"
],
"version": "==4.1.0"
"version": "==4.2.0"
},
"pexpect": {
"hashes": [
@ -335,12 +615,10 @@
},
"pluggy": {
"hashes": [
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff",
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c",
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
],
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*'",
"version": "==0.6.0"
"version": "==0.7.1"
},
"prompt-toolkit": {
"hashes": [
@ -362,7 +640,6 @@
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
],
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*'",
"version": "==1.5.4"
},
"pygments": {
@ -374,27 +651,27 @@
},
"pylint": {
"hashes": [
"sha256:285c9ae7255acb584057fe31051a37498985e632b99fc0ec93b7eb38a1b137f9",
"sha256:dd5c0fc4e6a4cb9483a4367699099a7dfc8a13de9ecac4cb16855ffac68d49de"
"sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec",
"sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb"
],
"index": "pypi",
"version": "==2.0.0.dev2"
"version": "==2.1.1"
},
"pytest": {
"hashes": [
"sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d",
"sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a"
"sha256:86a8dbf407e437351cef4dba46736e9c5a6e3c3ac71b2e942209748e76ff2086",
"sha256:e74466e97ac14582a8188ff4c53e6cc3810315f342f6096899332ae864c1d432"
],
"index": "pypi",
"version": "==3.6.0"
"version": "==3.7.1"
},
"pytest-tornado": {
"pytest-flask": {
"hashes": [
"sha256:214fc59d06fb81696fce3028b56dff522168ac1cfc784cfc0077b7b1e425b4cd",
"sha256:687c1f9c0f5bda7808c1e53c14bbebfe4fb9452e34cc95b440e598d4724265e0"
"sha256:2c5a36f9033ef8b6f85ddbefaebdd4f89197fc283f94b20dfe1a1beba4b77f03",
"sha256:657c7de386215ab0230bee4d76ace0339ae82fcbb34e134e17a29f65032eef03"
],
"index": "pypi",
"version": "==0.5.0"
"version": "==0.10.0"
},
"pytest-watch": {
"hashes": [
@ -403,13 +680,24 @@
"index": "pypi",
"version": "==4.2.0"
},
"python-dateutil": {
"hashes": [
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
],
"version": "==2.7.3"
},
"pyyaml": {
"hashes": [
"sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110",
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
"sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7",
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b"
"sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b",
"sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d"
],
"version": "==4.2b4"
},
@ -428,17 +716,24 @@
},
"smmap2": {
"hashes": [
"sha256:b78ee0f1f5772d69ff50b1cbdb01b8c6647a8354f02f23b488cf4b2cfc923956",
"sha256:c7530db63f15f09f8251094b22091298e82bf6c699a6b8344aaaef3f2e1276c3"
"sha256:0dd53d991af487f9b22774fa89451358da3607c02b9b886a54736c6a313ece0b",
"sha256:dc216005e529d57007ace27048eb336dcecb7fc413cfb3b2f402bb25972b69c6"
],
"version": "==2.0.3"
"version": "==2.0.4"
},
"stevedore": {
"hashes": [
"sha256:e3d96b2c4e882ec0c1ff95eaebf7b575a779fd0ccb4c741b9832bed410d58b3d",
"sha256:f1c7518e7b160336040fee272174f1f7b29a46febb3632502a8f2055f973d60b"
"sha256:1e153545aca7a6a49d8337acca4f41c212fbfa60bf864ecd056df0cafb9627e8",
"sha256:c7eac1c0d95824c88b655273da5c17cdde6482b2739f47c30bf851dcc9d3c2c0"
],
"version": "==1.28.0"
"version": "==1.29.0"
},
"text-unidecode": {
"hashes": [
"sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d",
"sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc"
],
"version": "==1.2"
},
"toml": {
"hashes": [
@ -446,17 +741,6 @@
],
"version": "==0.9.4"
},
"tornado": {
"hashes": [
"sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7",
"sha256:408d129e9d13d3c55aa73f8084aa97d5f90ed84132e38d6932e63a67d5bec563",
"sha256:88ce0282cce70df9045e515f578c78f1ebc35dcabe1d70f800c3583ebda7f5f5",
"sha256:ba9fbb249ac5390bff8a1d6aa4b844fd400701069bda7d2e380dfe2217895101",
"sha256:c050089173c2e9272244bccfb6a8615fb9e53b79420a5551acfa76094ecc3111"
],
"index": "pypi",
"version": "==5.0.2"
},
"traitlets": {
"hashes": [
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
@ -490,9 +774,16 @@
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
],
"markers": "python_version < '3.7' and implementation_name == 'cpython'",
"version": "==1.1.0"
},
"typing": {
"hashes": [
"sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf",
"sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8",
"sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2"
],
"version": "==3.6.4"
},
"watchdog": {
"hashes": [
"sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162"
@ -506,6 +797,13 @@
],
"version": "==0.1.7"
},
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
},
"wrapt": {
"hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"

View File

@ -1,22 +1,22 @@
# ATST
[![Build Status](https://travis-ci.org/dod-ccpo/atst.svg?branch=master)](https://travis-ci.org/dod-ccpo/atst)
## Description
This is the main user-facing web application for the ATAT stack. All end-user
requests are handled by ATST, with it making backend calls to various
microservices when appropriate.
This is the user-facing web application for ATAT.
## Installation
### Requirements
See the [scriptz](https://github.com/dod-ccpo/scriptz) repository for the shared
requirements and guidelines for all ATAT applications.
Additionally, ATST requires a redis instance for session management. Have redis
installed and running. By default, ATST will try to connect to a redis instance
running on localhost on its default port, 6379.
ATST requires a postgres instance (>= 9.6) for persistence. Have postgres installed
and running on the default port of 5432.
ATST also requires a redis instance for session management. Have redis installed and
running on the default port of 6379.
### Cloning
This project contains git submodules. Here is an example clone command that will
@ -58,6 +58,10 @@ To start the app locally in the foreground and watch for changes:
script/dev_server
To watch for changes to any js/css assets:
yarn watch
### Users
There are currently six mock users for development:
@ -73,6 +77,12 @@ To log in as one of them, navigate to `/login-dev?username=<lowercase name>`. Fo
## Testing
Tests require a test database:
```
createdb atat_test
```
To run lint, static analysis, and unit tests:
script/test
@ -87,14 +97,21 @@ To re-run tests each time a file is changed:
## Notes
tornado templates are like mustache templates -- add the
Jinja templates are like mustache templates -- add the
following to `~/.vim/filetype.vim` for syntax highlighting:
:au BufRead *.html.to set filetype=mustache
## Icons
To render an icon use `{% module Icon('name') %}` in a template, where `name` is the filename of an svg file in `static/icons`.
To render an icon, use
```jinja
{% import "components/icon.html" %}
{{ Icon("icon-name", classes="css-classes") }}
```
where `icon-name` is the filename of an svg in `static/icons`.
All icons used should be from the Noun Project, specifically [this collection](https://thenounproject.com/monstercritic/collection/tinicons-a-set-of-tiny-icons-perfect-for-ui-elemen/) if possible.
@ -104,7 +121,7 @@ SVG markup should be cleaned an minified, [Svgsus](http://www.svgs.us/) works we
The `/login-dev` endpoint is protected by HTTP basic auth when deployed. This can be configured for NGINX following the instructions [here](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/). The following config should added within the main server block for the site:
```
```nginx
location /login-dev {
auth_basic "Developer Access";
auth_basic_user_file /etc/apache2/.htpasswd;

74
alembic.ini Normal file
View File

@ -0,0 +1,74 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
alembic/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

84
alembic/env.py Normal file
View File

@ -0,0 +1,84 @@
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
import sys
from unipath import Path
parent_dir = Path(__file__).parent.parent
sys.path.append(parent_dir)
from atst.app import make_config
app_config = make_config()
config.set_main_option('sqlalchemy.url', app_config['DATABASE_URI'])
from atst.database import db
from atst.models import *
target_metadata = Base.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
alembic/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,38 @@
"""update_user_from_authnid
Revision ID: 1f57f784ed5b
Revises: 55ba973d08b9
Create Date: 2018-07-30 16:53:05.945005
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1f57f784ed5b'
down_revision = '55ba973d08b9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('dod_id', sa.String(), nullable=True))
op.add_column('users', sa.Column('email', sa.String(), nullable=True))
op.add_column('users', sa.Column('first_name', sa.String(), nullable=True))
op.add_column('users', sa.Column('last_name', sa.String(), nullable=True))
op.create_unique_constraint('users_dod_id_unique', 'users', ['dod_id'])
op.create_unique_constraint('users_email_unqiue', 'users', ['email'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('users_dod_id_unique', 'users', type_='unique')
op.drop_constraint('users_email_unqiue', 'users', type_='unique')
op.drop_column('users', 'last_name')
op.drop_column('users', 'first_name')
op.drop_column('users', 'email')
op.drop_column('users', 'dod_id')
# ### end Alembic commands ###

View File

@ -0,0 +1,39 @@
"""add_default_atat_role
Revision ID: 4ea5917e7781
Revises: 96a9f3537996
Create Date: 2018-07-30 13:51:29.576931
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm.session import Session
# revision identifiers, used by Alembic.
revision = '4ea5917e7781'
down_revision = '96a9f3537996'
branch_labels = None
depends_on = None
from atst.models.role import Role
from atst.models.permissions import Permissions
def upgrade():
session = Session(bind=op.get_bind())
mission_owner_role = Role(
name='default',
description='',
permissions=[
Permissions.REQUEST_JEDI_WORKSPACE,
]
)
session.add(mission_owner_role)
session.commit()
def downgrade():
db = op.get_bind()
db.execute("DELETE FROM roles WHERE name = 'default'")

View File

@ -0,0 +1,62 @@
"""add_authz_models
Revision ID: 4ede1e3e50d1
Revises: b5b17d465166
Create Date: 2018-07-30 11:34:12.016857
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '4ede1e3e50d1'
down_revision = 'b5b17d465166'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('roles',
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('permissions', postgresql.ARRAY(sa.String()), server_default='{}', nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=True)
op.create_index(op.f('ix_roles_permissions'), 'roles', ['permissions'], unique=False)
op.create_table('users',
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('username', sa.String(), nullable=True),
sa.Column('atat_role_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.ForeignKeyConstraint(['atat_role_id'], ['roles.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('workspace_role',
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('workspace_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('role_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_workspace_role_user_id'), 'workspace_role', ['user_id'], unique=False)
op.create_index(op.f('ix_workspace_role_workspace_id'), 'workspace_role', ['workspace_id'], unique=False)
op.create_index('workspace_role_user_workspace', 'workspace_role', ['user_id', 'workspace_id'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('workspace_role_user_workspace', table_name='workspace_role')
op.drop_index(op.f('ix_workspace_role_workspace_id'), table_name='workspace_role')
op.drop_index(op.f('ix_workspace_role_user_id'), table_name='workspace_role')
op.drop_table('workspace_role')
op.drop_table('users')
op.drop_index(op.f('ix_roles_permissions'), table_name='roles')
op.drop_index(op.f('ix_roles_name'), table_name='roles')
op.drop_table('roles')
# ### end Alembic commands ###

View File

@ -0,0 +1,38 @@
"""add_fundz_models
Revision ID: 55ba973d08b9
Revises: 4ea5917e7781
Create Date: 2018-07-30 14:43:34.099799
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '55ba973d08b9'
down_revision = '4ea5917e7781'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('pe_number',
sa.Column('number', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('number')
)
op.create_table('task_order',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('number', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('task_order')
op.drop_table('pe_number')
# ### end Alembic commands ###

View File

@ -0,0 +1,183 @@
"""add_roles_and_permissions
Revision ID: 96a9f3537996
Revises: 4ede1e3e50d1
Create Date: 2018-07-30 13:48:31.325234
"""
import os
import sys
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm.session import Session
from atst.models.role import Role
from atst.models.permissions import Permissions
# revision identifiers, used by Alembic.
revision = '96a9f3537996'
down_revision = '4ede1e3e50d1'
branch_labels = None
depends_on = None
def upgrade():
session = Session(bind=op.get_bind())
roles = [
Role(
name='ccpo',
description='',
permissions=[
Permissions.VIEW_ORIGINAL_JEDI_REQEUST,
Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST,
Permissions.MODIFY_ATAT_ROLE_PERMISSIONS,
Permissions.CREATE_CSP_ROLE,
Permissions.DELETE_CSP_ROLE,
Permissions.DEACTIVE_CSP_ROLE,
Permissions.MODIFY_CSP_ROLE_PERMISSIONS,
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
Permissions.REMOVE_CSP_ROLES,
Permissions.REQUEST_NEW_CSP_ROLE,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
Permissions.DEACTIVATE_WORKSPACE,
Permissions.VIEW_ATAT_PERMISSIONS,
Permissions.TRANSFER_OWNERSHIP_OF_WORKSPACE,
Permissions.ADD_APPLICATION_IN_WORKSPACE,
Permissions.DELETE_APPLICATION_IN_WORKSPACE,
Permissions.DEACTIVATE_APPLICATION_IN_WORKSPACE,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
Permissions.RENAME_APPLICATION_IN_WORKSPACE,
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
Permissions.ADD_TAG_TO_WORKSPACE,
Permissions.REMOVE_TAG_FROM_WORKSPACE
]
),
Role(
name='owner',
description='',
permissions=[
Permissions.REQUEST_JEDI_WORKSPACE,
Permissions.VIEW_ORIGINAL_JEDI_REQEUST,
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
Permissions.REMOVE_CSP_ROLES,
Permissions.REQUEST_NEW_CSP_ROLE,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
Permissions.DEACTIVATE_WORKSPACE,
Permissions.VIEW_ATAT_PERMISSIONS,
Permissions.ADD_APPLICATION_IN_WORKSPACE,
Permissions.DELETE_APPLICATION_IN_WORKSPACE,
Permissions.DEACTIVATE_APPLICATION_IN_WORKSPACE,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
Permissions.RENAME_APPLICATION_IN_WORKSPACE,
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
]
),
Role(
name='admin',
description='',
permissions=[
Permissions.VIEW_USAGE_REPORT,
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
Permissions.REMOVE_CSP_ROLES,
Permissions.REQUEST_NEW_CSP_ROLE,
Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE,
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
Permissions.ADD_APPLICATION_IN_WORKSPACE,
Permissions.DELETE_APPLICATION_IN_WORKSPACE,
Permissions.DEACTIVATE_APPLICATION_IN_WORKSPACE,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
Permissions.RENAME_APPLICATION_IN_WORKSPACE,
Permissions.ADD_ENVIRONMENT_IN_APPLICATION,
Permissions.DELETE_ENVIRONMENT_IN_APPLICATION,
Permissions.DEACTIVATE_ENVIRONMENT_IN_APPLICATION,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
Permissions.RENAME_ENVIRONMENT_IN_APPLICATION,
]
),
Role(
name='developer',
description='',
permissions=[
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION
]
),
Role(
name='billing_auditor',
description='',
permissions=[
Permissions.VIEW_USAGE_REPORT,
Permissions.VIEW_USAGE_DOLLARS,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
]
),
Role(
name='security_auditor',
description='',
permissions=[
Permissions.VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS,
Permissions.VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS,
Permissions.VIEW_ATAT_PERMISSIONS,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
Permissions.VIEW_ENVIRONMENT_IN_APPLICATION,
]
),
]
session.add_all(roles)
session.commit()
def downgrade():
db = op.get_bind()
db.execute("""
DELETE FROM roles
WHERE name IN (
'ccpo',
'owner',
'admin',
'developer',
'billing_auditor',
'security_auditor'
);
""")

View File

@ -0,0 +1,47 @@
"""requests
Revision ID: b5b17d465166
Revises: ff1c9c02fa61
Create Date: 2018-07-23 14:58:05.044456
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'b5b17d465166'
down_revision = 'ff1c9c02fa61'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('requests',
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('creator', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('time_created', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('body', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('request_status_events',
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('new_status', sa.String(), nullable=True),
sa.Column('time_created', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('request_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('sequence', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(['request_id'], ['requests.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
db = op.get_bind()
db.execute("CREATE SEQUENCE request_status_events_sequence_seq OWNED BY request_status_events.sequence;")
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('request_status_events')
op.drop_table('requests')
# ### end Alembic commands ###

View File

@ -0,0 +1,26 @@
"""enable uuid extension
Revision ID: ff1c9c02fa61
Revises:
Create Date: 2018-07-23 14:54:05.422286
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ff1c9c02fa61'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
connection = op.get_bind()
connection.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
def downgrade():
connection = op.get_bind()
connection.execute('DROP EXTENSION IF EXISTS "uuid-ossp"')

12
app.py
View File

@ -1,15 +1,11 @@
#!/usr/bin/env python
import tornado.ioloop
from atst.app import make_app, make_deps, make_config
from atst.app import make_app, make_config
config = make_config()
deps = make_deps(config)
app = make_app(config, deps)
app = make_app(config)
if __name__ == "__main__":
port = int(config["default"]["PORT"])
app.listen(port)
port = int(config["PORT"])
app.run(port=port)
print("Listening on http://localhost:%i" % port)
tornado.ioloop.IOLoop.current().start()

View File

@ -1,60 +0,0 @@
import tornado.gen
from tornado.httpclient import AsyncHTTPClient
from json import dumps, loads, decoder
class ApiClient(object):
def __init__(self, base_url, api_version=None, validate_cert=True):
self.base_url = base_url
if api_version:
self.base_url = f"{base_url}/api/{api_version}"
self.client = AsyncHTTPClient()
self.validate_cert = validate_cert
@tornado.gen.coroutine
def get(self, path, **kwargs):
return (yield self.make_request("GET", self.base_url + path, **kwargs))
@tornado.gen.coroutine
def put(self, path, **kwargs):
return (yield self.make_request("PUT", self.base_url + path, **kwargs))
@tornado.gen.coroutine
def post(self, path, **kwargs):
return (yield self.make_request("POST", self.base_url + path, **kwargs))
@tornado.gen.coroutine
def patch(self, path, **kwargs):
return (yield self.make_request("PATCH", self.base_url + path, **kwargs))
@tornado.gen.coroutine
def delete(self, path, **kwargs):
return (yield self.make_request("DELETE", self.base_url + path, **kwargs))
@tornado.gen.coroutine
def make_request(self, method, url, **kwargs):
# If 'json' kwarg is specified, serialize it to 'body' and update
# the Content-Type.
if "json" in kwargs:
kwargs["body"] = dumps(kwargs["json"])
del kwargs["json"]
headers = kwargs.get("headers", {})
headers["Content-Type"] = "application/json"
kwargs["headers"] = headers
if not "validate_cert" in kwargs:
kwargs["validate_cert"] = self.validate_cert
response = yield self.client.fetch(url, method=method, **kwargs)
return self.adapt_response(response)
def adapt_response(self, response):
if "application/json" in response.headers["Content-Type"]:
try:
json = loads(response.body)
setattr(response, "json", json)
except decoder.JSONDecodeError:
setattr(response, "json", {})
else:
setattr(response, "json", {})
setattr(response, "ok", 200 <= response.code < 300)
return response

View File

@ -1,187 +1,96 @@
import os
import re
import pathlib
from configparser import ConfigParser
import tornado.web
from tornado.web import url
from redis import StrictRedis
from flask import Flask, request, g
from flask_session import Session
import redis
from unipath import Path
from flask_wtf.csrf import CSRFProtect
from atst.handlers.main import Main
from atst.handlers.root import Root
from atst.handlers.login_redirect import LoginRedirect
from atst.handlers.workspaces import Workspaces
from atst.handlers.workspace import Workspace
from atst.handlers.workspace_members import WorkspaceMembers
from atst.handlers.request import Request
from atst.handlers.request_financial_verification import RequestFinancialVerification
from atst.handlers.request_new import RequestNew
from atst.handlers.request_submit import RequestsSubmit
from atst.handlers.dev import Dev
from atst.home import home
from atst.api_client import ApiClient
from atst.sessions import RedisSessions
from atst import ui_modules
from atst import ui_methods
from atst.database import db
from atst.assets import environment as assets_environment
ENV = os.getenv("TORNADO_ENV", "dev")
from atst.routes import bp
from atst.routes.workspaces import bp as workspace_routes
from atst.routes.requests import requests_bp
from atst.routes.dev import bp as dev_routes
from atst.domain.authnid.crl.validator import Validator
from atst.domain.auth import apply_authentication
def make_app(config, deps, **kwargs):
routes = [
url(r"/", Root, {"page": "root"}, name="root"),
url(
r"/login-redirect",
LoginRedirect,
{"sessions": deps["sessions"], "authnid_client": deps["authnid_client"], "authz_client": deps["authz_client"]},
name="login_redirect",
),
url(r"/home", Main, {"page": "home"}, name="home"),
url(
r"/styleguide",
Main,
{"page": "styleguide"},
name="styleguide",
),
url(
r"/workspaces/blank",
Main,
{"page": "workspaces_blank"},
name="workspaces_blank",
),
url(
r"/workspaces",
Workspaces,
{"page": "workspaces", "authz_client": deps["authz_client"]},
name="workspaces",
),
url(
r"/requests",
Request,
{"page": "requests", "requests_client": deps["requests_client"]},
name="requests",
),
url(
r"/requests/new",
RequestNew,
{
"page": "requests_new",
"requests_client": deps["requests_client"],
"fundz_client": deps["fundz_client"],
},
name="request_new",
),
url(
r"/requests/new/([0-9])",
RequestNew,
{
"page": "requests_new",
"requests_client": deps["requests_client"],
"fundz_client": deps["fundz_client"],
},
name="request_form_new",
),
url(
r"/requests/new/([0-9])/(\S+)",
RequestNew,
{
"page": "requests_new",
"requests_client": deps["requests_client"],
"fundz_client": deps["fundz_client"],
},
name="request_form_update",
),
url(
r"/requests/submit/(\S+)",
RequestsSubmit,
{"requests_client": deps["requests_client"]},
name="requests_submit",
),
# Dummy request/approval screen
url(
r"/request/approval",
Main,
{"page": "request_approval"},
name="request_approval"
),
url(
r"/requests/verify/(\S+)",
RequestFinancialVerification,
{
"page": "financial_verification",
"requests_client": deps["requests_client"],
"fundz_client": deps["fundz_client"],
},
name="financial_verification",
),
url(
r"/requests/financial_verification_submitted",
Main,
{"page": "requests/financial_verification_submitted"},
name="financial_verification_submitted",
),
url(r"/users", Main, {"page": "users"}, name="users"),
url(r"/reports", Main, {"page": "reports"}, name="reports"),
url(r"/calculator", Main, {"page": "calculator"}, name="calculator"),
url(r"/workspaces/(\S+)/members", WorkspaceMembers, {}, name="workspace_members"),
url(r"/workspaces/(\S+)/projects", Workspace, {}, name="workspace_projects"),
url(r"/workspaces/123456/projects/789/edit", Main, {"page": "project_edit"}, name="project_edit"),
url(r"/workspaces/123456/members/789/edit", Main, {"page": "member_edit"}, name="member_edit"),
]
ENV = os.getenv("FLASK_ENV", "dev")
if not ENV == "production":
routes += [
url(
r"/login-dev",
Dev,
{"action": "login", "sessions": deps["sessions"], "authz_client": deps["authz_client"]},
name="dev-login",
)
]
app = tornado.web.Application(
routes,
login_url="/",
template_path=home.child("templates"),
static_path=home.child("static"),
cookie_secret=config["default"]["COOKIE_SECRET"],
debug=config["default"].getboolean("DEBUG"),
ui_modules=ui_modules,
ui_methods=ui_methods,
**kwargs,
def make_app(config):
parent_dir = Path().parent
app = Flask(
__name__,
template_folder=parent_dir.child("templates").absolute(),
static_folder=parent_dir.child("static").absolute(),
)
app.config = config
app.sessions = deps["sessions"]
redis = make_redis(config)
csrf = CSRFProtect()
app.config.update(config)
app.config.update({"SESSION_REDIS": redis})
make_flask_callbacks(app)
make_crl_validator(app)
db.init_app(app)
csrf.init_app(app)
Session(app)
assets_environment.init_app(app)
app.register_blueprint(bp)
app.register_blueprint(workspace_routes)
app.register_blueprint(requests_bp)
if ENV != "production":
app.register_blueprint(dev_routes)
apply_authentication(app)
return app
def make_deps(config):
# we do not want to do SSL verify services in test and development
validate_cert = ENV == "production"
redis_client = StrictRedis.from_url(
config["default"]["REDIS_URI"], decode_responses=True
)
def make_flask_callbacks(app):
@app.before_request
def _set_globals():
g.navigationContext = (
"workspace"
if re.match("\/workspaces\/[A-Za-z0-9]*", request.url)
else "global"
)
g.dev = os.getenv("FLASK_ENV", "dev") == "dev"
g.matchesPath = lambda href: re.match("^" + href, request.path)
g.modalOpen = request.args.get("modal", False)
g.current_user = {
"id": "cce17030-4109-4719-b958-ed109dbb87c8",
"first_name": "Amanda",
"last_name": "Adamson",
"atat_role": "default",
"atat_permissions": [],
}
@app.template_filter('iconSvg')
def _iconSvg(name):
with open('static/icons/'+name+'.svg') as contents:
return contents.read()
def map_config(config):
return {
"authz_client": ApiClient(
config["default"]["AUTHZ_BASE_URL"],
api_version="v1",
validate_cert=validate_cert,
),
"authnid_client": ApiClient(
config["default"]["AUTHNID_BASE_URL"],
api_version="v1",
validate_cert=validate_cert,
),
"fundz_client": ApiClient(
config["default"]["FUNDZ_BASE_URL"],
validate_cert=validate_cert,
),
"requests_client": ApiClient(
config["default"]["REQUESTS_QUEUE_BASE_URL"],
api_version="v1",
validate_cert=validate_cert,
),
"sessions": RedisSessions(
redis_client, config["default"]["SESSION_TTL_SECONDS"]
),
**config["default"],
"ENV": config["default"]["ENVIRONMENT"],
"DEBUG": config["default"]["DEBUG"],
"PORT": int(config["default"]["PORT"]),
"SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"],
"SQLALCHEMY_TRACK_MODIFICATIONS": False,
"WTF_CSRF_ENABLED": config.getboolean("default", "WTF_CSRF_ENABLED"),
"PERMANENT_SESSION_LIFETIME": config.getint("default", "PERMANENT_SESSION_LIFETIME"),
}
@ -190,8 +99,49 @@ def make_config():
ENV_CONFIG_FILENAME = os.path.join(
os.path.dirname(__file__), "../config/", "{}.ini".format(ENV.lower())
)
OVERRIDE_CONFIG_FILENAME = os.getenv("OVERRIDE_CONFIG_FULLPATH")
config = ConfigParser()
config.optionxform = str
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
if OVERRIDE_CONFIG_FILENAME:
config_files.append(OVERRIDE_CONFIG_FILENAME)
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
if OVERRIDE_CONFIG_FILENAME:
config_files.append(OVERRIDE_CONFIG_FILENAME)
# ENV_CONFIG will override values in BASE_CONFIG.
config.read([BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME])
return config
config.read(config_files)
# Assemble DATABASE_URI value
database_uri = (
"postgres://"
+ config.get("default", "PGUSER")
+ ":"
+ config.get("default", "PGPASSWORD")
+ "@"
+ config.get("default", "PGHOST")
+ ":"
+ config.get("default", "PGPORT")
+ "/"
+ config.get("default", "PGDATABASE")
)
config.set("default", "DATABASE_URI", database_uri)
return map_config(config)
def make_redis(config):
return redis.Redis.from_url(config['REDIS_URI'])
def make_crl_validator(app):
crl_locations = []
for filename in pathlib.Path(app.config["CRL_DIRECTORY"]).glob("*"):
crl_locations.append(filename.absolute())
app.crl_validator = Validator(
roots=[app.config["CA_CHAIN"]], crl_locations=crl_locations
)
for e in app.crl_validator.errors:
app.logger.error(e)

View File

@ -1,16 +1,16 @@
from webassets import Environment, Bundle
from atst.home import home
from flask_assets import Environment, Bundle
environment = Environment(
directory=home.child("scss"),
url="/static"
)
environment = Environment()
css = Bundle(
"atat.scss",
filters="scss",
output="../static/assets/out.%(version)s.css",
depends=("**/*.scss"),
"../static/assets/index.css",
output="../static/assets/index.%(version)s.css",
)
environment.register("css", css)
js = Bundle(
'../static/assets/index.js',
output='../static/assets/index.%(version)s.js'
)
environment.register('js_all', js)

3
atst/database.py Normal file
View File

@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

32
atst/domain/auth.py Normal file
View File

@ -0,0 +1,32 @@
from flask import g, redirect, url_for, session, request
from atst.domain.users import Users
UNPROTECTED_ROUTES = ["atst.root", "dev.login_dev", "atst.login_redirect", "atst.unauthorized", "static"]
def apply_authentication(app):
@app.before_request
# pylint: disable=unused-variable
def enforce_login():
if not _unprotected_route(request):
user = get_current_user()
if user:
g.current_user = user
else:
return redirect(url_for("atst.root"))
def get_current_user():
user_id = session.get("user_id")
if user_id:
return Users.get(user_id)
else:
return False
def _unprotected_route(request):
if request.endpoint in UNPROTECTED_ROUTES:
return True

View File

View File

@ -0,0 +1,72 @@
import requests
import re
import os
from html.parser import HTMLParser
_DISA_CRLS = "https://iasecontent.disa.mil/pki-pke/data/crls/dod_crldps.htm"
def fetch_disa():
response = requests.get(_DISA_CRLS)
return response.text
class DISAParser(HTMLParser):
crl_list = []
_CRL_MATCH = re.compile("DOD(ROOT|EMAIL|ID)?CA")
def handle_starttag(self, tag, attrs):
if tag == "a":
href = [pair[1] for pair in attrs if pair[0] == "href"].pop()
if re.search(self._CRL_MATCH, href):
self.crl_list.append(href)
def crl_list_from_disa_html(html):
parser = DISAParser()
parser.reset()
parser.feed(html)
return parser.crl_list
def write_crl(out_dir, crl_location):
name = re.split("/", crl_location)[-1]
crl = os.path.join(out_dir, name)
with requests.get(crl_location, stream=True) as r:
with open(crl, "wb") as crl_file:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
crl_file.write(chunk)
def refresh_crls(out_dir, logger=None):
disa_html = fetch_disa()
crl_list = crl_list_from_disa_html(disa_html)
for crl_location in crl_list:
if logger:
logger.info("updating CRL from {}".format(crl_location))
try:
write_crl(out_dir, crl_location)
except requests.exceptions.ChunkedEncodingError:
if logger:
logger.error(
"Error downloading {}, continuing anyway".format(crl_location)
)
if __name__ == "__main__":
import sys
import datetime
import logging
logging.basicConfig(
level=logging.INFO, format="[%(asctime)s]:%(levelname)s: %(message)s"
)
logger = logging.getLogger()
logger.info("Updating CRLs")
try:
refresh_crls(sys.argv[1], logger=logger)
except Exception as err:
logger.exception("Fatal error encountered, stopping")
sys.exit(1)
logger.info("Finished updating CRLs")

View File

@ -0,0 +1,124 @@
import sys
import os
import re
import hashlib
from OpenSSL import crypto, SSL
def sha256_checksum(filename, block_size=65536):
sha256 = hashlib.sha256()
with open(filename, "rb") as f:
for block in iter(lambda: f.read(block_size), b""):
sha256.update(block)
return sha256.hexdigest()
class Validator:
_PEM_RE = re.compile(
b"-----BEGIN CERTIFICATE-----\r?.+?\r?-----END CERTIFICATE-----\r?\n?",
re.DOTALL,
)
def __init__(self, crl_locations=[], roots=[], base_store=crypto.X509Store):
self.errors = []
self.crl_locations = crl_locations
self.roots = roots
self.base_store = base_store
self._reset()
def _reset(self):
self.cache = {}
self.store = self.base_store()
self._add_crls(self.crl_locations)
self._add_roots(self.roots)
self.store.set_flags(crypto.X509StoreFlags.CRL_CHECK)
def _add_crls(self, locations):
for filename in locations:
try:
self._add_crl(filename)
except crypto.Error as err:
self.errors.append(
"CRL could not be parsed. Filename: {}, Error: {}, args: {}".format(
filename, type(err), err.args
)
)
# This caches the CRL issuer with the CRL filepath and a checksum, in addition to adding the CRL to the store.
def _add_crl(self, filename):
with open(filename, "rb") as crl_file:
crl = crypto.load_crl(crypto.FILETYPE_ASN1, crl_file.read())
self.cache[crl.get_issuer().der()] = (filename, sha256_checksum(filename))
self._add_carefully("add_crl", crl)
def _parse_roots(self, root_str):
return [match.group(0) for match in self._PEM_RE.finditer(root_str)]
def _add_roots(self, roots):
for filename in roots:
with open(filename, "rb") as f:
for raw_ca in self._parse_roots(f.read()):
ca = crypto.load_certificate(crypto.FILETYPE_PEM, raw_ca)
self._add_carefully("add_cert", ca)
# in testing, it seems that openssl is maintaining a local cache of certs
# in a hash table and throws errors if you try to add redundant certs or
# CRLs. For now, we catch and ignore that error with great specificity.
def _add_carefully(self, method_name, obj):
try:
getattr(self.store, method_name)(obj)
except crypto.Error as error:
if self._is_preloaded_error(error):
pass
else:
raise error
PRELOADED_CRL = (
[
(
"x509 certificate routines",
"X509_STORE_add_crl",
"cert already in hash table",
)
],
)
PRELOADED_CERT = (
[
(
"x509 certificate routines",
"X509_STORE_add_cert",
"cert already in hash table",
)
],
)
def _is_preloaded_error(self, error):
return error.args == self.PRELOADED_CRL or error.args == self.PRELOADED_CERT
# Checks that the CRL currently in-memory is up-to-date via the checksum.
def refresh_cache(self, cert):
der = cert.get_issuer().der()
if der in self.cache:
filename, checksum = self.cache[der]
if sha256_checksum(filename) != checksum:
self._reset()
def validate(self, cert):
parsed = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
self.refresh_cache(parsed)
context = crypto.X509StoreContext(self.store, parsed)
try:
context.verify_certificate()
return True
except crypto.X509StoreContextError as err:
self.errors.append(
"Certificate revoked or errored. Error: {}. Args: {}".format(
type(err), err.args
)
)
return False

View File

@ -0,0 +1,13 @@
import re
# TODO: our sample SDN does not have an email address
def parse_sdn(sdn):
try:
parts = sdn.split(",")
cn_string = [piece for piece in parts if re.match("^CN=", piece)][0]
cn = cn_string.split("=")[-1]
info = cn.split(".")
return {"last_name": info[0], "first_name": info[1], "dod_id": info[-1]}
except (IndexError, AttributeError):
raise ValueError("'{}' is not a valid SDN".format(sdn))

16
atst/domain/exceptions.py Normal file
View File

@ -0,0 +1,16 @@
class NotFoundError(Exception):
def __init__(self, resource_name):
self.resource_name = resource_name
@property
def message(self):
return "No {} could be found.".format(self.resource_name)
class AlreadyExistsError(Exception):
def __init__(self, resource_name):
self.resource_name = resource_name
@property
def message(self):
return "{} already exists".format(self.resource_name)

25
atst/domain/pe_numbers.py Normal file
View File

@ -0,0 +1,25 @@
from sqlalchemy.dialects.postgresql import insert
from atst.database import db
from atst.models.pe_number import PENumber
from .exceptions import NotFoundError
class PENumbers(object):
@classmethod
def get(cls, number):
pe_number = db.session.query(PENumber).get(number)
if not pe_number:
raise NotFoundError("pe_number")
return pe_number
@classmethod
def create_many(cls, list_of_pe_numbers):
stmt = insert(PENumber).values(list_of_pe_numbers)
do_update = stmt.on_conflict_do_update(
index_elements=["number"], set_=dict(description=stmt.excluded.description)
)
db.session.execute(do_update)
db.session.commit()

134
atst/domain/requests.py Normal file
View File

@ -0,0 +1,134 @@
from sqlalchemy import exists, and_
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.attributes import flag_modified
from atst.models import Request, RequestStatusEvent
from atst.database import db
from .exceptions import NotFoundError
def deep_merge(source, destination: dict):
"""
Merge source dict into destination dict recursively.
"""
def _deep_merge(a, b):
for key, value in a.items():
if isinstance(value, dict):
node = b.setdefault(key, {})
_deep_merge(value, node)
else:
b[key] = value
return b
return _deep_merge(source, dict(destination))
class Requests(object):
AUTO_APPROVE_THRESHOLD = 1000000
@classmethod
def create(cls, creator_id, body):
request = Request(creator=creator_id, body=body)
status_event = RequestStatusEvent(new_status="incomplete")
request.status_events.append(status_event)
db.session.add(request)
db.session.commit()
return request
@classmethod
def exists(cls, request_id, creator_id):
return db.session.query(
exists().where(
and_(Request.id == request_id, Request.creator == creator_id)
)
).scalar()
@classmethod
def get(cls, request_id):
try:
request = db.session.query(Request).filter_by(id=request_id).one()
except NoResultFound:
raise NotFoundError("request")
return request
@classmethod
def get_many(cls, creator_id=None):
filters = []
if creator_id:
filters.append(Request.creator == creator_id)
requests = (
db.session.query(Request)
.filter(*filters)
.order_by(Request.time_created.desc())
.all()
)
return requests
@classmethod
def submit(cls, request):
request.status_events.append(RequestStatusEvent(new_status="submitted"))
if Requests.should_auto_approve(request):
request.status_events.append(RequestStatusEvent(new_status="approved"))
db.session.add(request)
db.session.commit()
return request
@classmethod
def update(cls, request_id, request_delta):
try:
# Query for request matching id, acquiring a row-level write lock.
# https://www.postgresql.org/docs/10/static/sql-select.html#SQL-FOR-UPDATE-SHARE
request = (
db.session.query(Request)
.filter_by(id=request_id)
.with_for_update(of=Request)
.one()
)
except NoResultFound:
return
request.body = deep_merge(request_delta, request.body)
if Requests.should_allow_submission(request):
request.status_events.append(
RequestStatusEvent(new_status="pending_submission")
)
# Without this, sqlalchemy won't notice the change to request.body,
# since it doesn't track dictionary mutations by default.
flag_modified(request, "body")
db.session.add(request)
db.session.commit()
@classmethod
def should_auto_approve(cls, request):
try:
dollar_value = request.body["details_of_use"]["dollar_value"]
except KeyError:
return False
return dollar_value < cls.AUTO_APPROVE_THRESHOLD
@classmethod
def should_allow_submission(cls, request):
all_request_sections = [
"details_of_use",
"information_about_you",
"primary_poc",
]
existing_request_sections = request.body.keys()
return request.status == "incomplete" and all(
section in existing_request_sections for section in all_request_sections
)

21
atst/domain/roles.py Normal file
View File

@ -0,0 +1,21 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models import Role
from .exceptions import NotFoundError
class Roles(object):
@classmethod
def get(cls, role_name):
try:
role = db.session.query(Role).filter_by(name=role_name).one()
except NoResultFound:
raise NotFoundError("role")
return role
@classmethod
def get_all(cls):
return db.session.query(Role).all()

View File

@ -0,0 +1,19 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.task_order import TaskOrder
from .exceptions import NotFoundError
class TaskOrders(object):
@classmethod
def get(self, order_number):
try:
task_order = (
db.session.query(TaskOrder).filter_by(number=order_number).one()
)
except NoResultFound:
raise NotFoundError("task_order")
return task_order

65
atst/domain/users.py Normal file
View File

@ -0,0 +1,65 @@
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.exc import IntegrityError
from atst.database import db
from atst.models import User
from .roles import Roles
from .exceptions import NotFoundError, AlreadyExistsError
class Users(object):
@classmethod
def get(cls, user_id):
try:
user = db.session.query(User).filter_by(id=user_id).one()
except NoResultFound:
raise NotFoundError("user")
return user
@classmethod
def get_by_dod_id(cls, dod_id):
try:
user = db.session.query(User).filter_by(dod_id=dod_id).one()
except NoResultFound:
raise NotFoundError("user")
return user
@classmethod
def create(cls, atat_role_name="developer", **kwargs):
atat_role = Roles.get(atat_role_name)
try:
user = User(atat_role=atat_role, **kwargs)
db.session.add(user)
db.session.commit()
except IntegrityError:
raise AlreadyExistsError("user")
return user
@classmethod
def get_or_create_by_dod_id(cls, dod_id, **kwargs):
try:
user = Users.get_by_dod_id(dod_id)
except NotFoundError:
user = Users.create(dod_id=dod_id, **kwargs)
db.session.add(user)
db.session.commit()
return user
@classmethod
def update(cls, user_id, atat_role_name):
user = Users.get(user_id)
atat_role = Roles.get(atat_role_name)
user.atat_role = atat_role
db.session.add(user)
db.session.commit()
return user

View File

@ -0,0 +1,73 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.workspace_role import WorkspaceRole
from atst.models.workspace_user import WorkspaceUser
from atst.models.user import User
from .roles import Roles
from .users import Users
from .exceptions import NotFoundError
class WorkspaceUsers(object):
@classmethod
def get(cls, workspace_id, user_id):
try:
user = Users.get(user_id)
except NoResultFound:
raise NotFoundError("user")
try:
workspace_role = (
WorkspaceRole.query.join(User)
.filter(User.id == user_id, WorkspaceRole.workspace_id == workspace_id)
.one()
)
except NoResultFound:
workspace_role = None
return WorkspaceUser(user, workspace_role)
@classmethod
def add_many(cls, workspace_id, workspace_user_dicts):
workspace_users = []
for user_dict in workspace_user_dicts:
try:
user = Users.get(user_dict["id"])
except NoResultFound:
default_role = Roles.get("developer")
user = User(id=user_dict["id"], atat_role=default_role)
try:
role = Roles.get(user_dict["workspace_role"])
except NoResultFound:
raise NotFoundError("role")
try:
existing_workspace_role = (
db.session.query(WorkspaceRole)
.filter(
WorkspaceRole.user == user,
WorkspaceRole.workspace_id == workspace_id,
)
.one()
)
new_workspace_role = existing_workspace_role
new_workspace_role.role = role
except NoResultFound:
new_workspace_role = WorkspaceRole(
user=user, role_id=role.id, workspace_id=workspace_id
)
user.workspace_roles.append(new_workspace_role)
workspace_user = WorkspaceUser(user, new_workspace_role)
workspace_users.append(workspace_user)
db.session.add(user)
db.session.commit()
return workspace_users

View File

@ -1,8 +1,4 @@
import tornado.gen
class Projects(object):
def __init__(self):
pass
@ -50,7 +46,6 @@ class Projects(object):
class Members(object):
def __init__(self):
pass

View File

@ -85,7 +85,7 @@ class MockEDAClient(EDAClientBase):
"location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=",
"pay_dodaac": None,
"pco_mod": "02",
"amount": 2000000
"amount": 2000000,
}
else:
return None

View File

@ -7,10 +7,7 @@ import pendulum
class DateField(DateField):
def _value(self):
if self.data:
date_formats = [
"YYYY-MM-DD",
"MM/DD/YYYY"
]
date_formats = ["YYYY-MM-DD", "MM/DD/YYYY"]
for _format in date_formats:
try:
return pendulum.from_format(self.data, _format).date()

View File

@ -1,22 +1,27 @@
import re
import tornado
from tornado.gen import Return
from wtforms.fields.html5 import EmailField
from wtforms.fields import StringField, SelectField
from wtforms.form import Form
from wtforms.validators import Required, Email
from atst.domain.exceptions import NotFoundError
from atst.domain.pe_numbers import PENumbers
from .fields import NewlineListField
from .forms import ValidatedForm
PE_REGEX = re.compile(r"""
PE_REGEX = re.compile(
r"""
(0?\d) # program identifier
(0?\d) # category
(\d) # activity
(\d+) # sponsor element
(.+) # service
""", re.X)
""",
re.X,
)
def suggest_pe_id(pe_id):
suggestion = pe_id
@ -34,33 +39,28 @@ def suggest_pe_id(pe_id):
return None
@tornado.gen.coroutine
def validate_pe_id(field, existing_request, fundz_client):
response = yield fundz_client.get(
"/pe-number/{}".format(field.data),
raise_error=False,
)
if not response.ok:
def validate_pe_id(field, existing_request):
try:
pe_number = PENumbers.get(field.data)
except NotFoundError:
suggestion = suggest_pe_id(field.data)
error_str = (
"We couldn't find that PE number. {}"
"If you have double checked it you can submit anyway. "
"Your request will need to go through a manual review."
).format("Did you mean \"{}\"? ".format(suggestion) if suggestion else "")
field.errors.append(error_str)
).format('Did you mean "{}"? '.format(suggestion) if suggestion else "")
field.errors += (error_str,)
return False
return True
class FinancialForm(ValidatedForm):
@tornado.gen.coroutine
def perform_extra_validation(self, existing_request, fundz_client):
def perform_extra_validation(self, existing_request):
valid = True
if not existing_request or existing_request.get('pe_id') != self.pe_id.data:
valid = yield validate_pe_id(self.pe_id, existing_request, fundz_client)
raise Return(valid)
if not existing_request or existing_request.get("pe_id") != self.pe_id.data:
valid = validate_pe_id(self.pe_id, existing_request)
return valid
task_order_id = StringField(
"Task Order Number associated with this request.", validators=[Required()]
@ -70,9 +70,7 @@ class FinancialForm(ValidatedForm):
"Unique Item Identifier (UII)s related to your application(s) if you already have them."
)
pe_id = StringField(
"Program Element (PE) Number related to your request"
)
pe_id = StringField("Program Element (PE) Number related to your request")
treasury_code = StringField("Program Treasury Code")
@ -118,11 +116,13 @@ class FinancialForm(ValidatedForm):
)
clin_0001 = StringField(
"<dl><dt>CLIN 0001</dt> - <dd>Unclassified IaaS and PaaS Amount</dd></dl>", validators=[Required()]
"<dl><dt>CLIN 0001</dt> - <dd>Unclassified IaaS and PaaS Amount</dd></dl>",
validators=[Required()],
)
clin_0003 = StringField(
"<dl><dt>CLIN 0003</dt> - <dd>Unclassified Cloud Support Package</dd></dl>", validators=[Required()]
"<dl><dt>CLIN 0003</dt> - <dd>Unclassified Cloud Support Package</dd></dl>",
validators=[Required()],
)
clin_1001 = StringField(

View File

@ -1,12 +1,8 @@
import tornado
from tornado.gen import Return
from wtforms_tornado import Form
from flask_wtf import FlaskForm
class ValidatedForm(Form):
@tornado.gen.coroutine
class ValidatedForm(FlaskForm):
def perform_extra_validation(self, *args, **kwargs):
"""A coroutine that performs any applicable extra validation. Must
"""Performs any applicable extra validation. Must
return True if the form is valid or False otherwise."""
raise Return(True)
return True

View File

@ -8,30 +8,15 @@ from .validators import DateRange, PhoneNumber, Alphabet
class OrgForm(ValidatedForm):
fname_request = StringField(
"First Name",
validators=[Required(), Alphabet()]
)
fname_request = StringField("First Name", validators=[Required(), Alphabet()])
lname_request = StringField(
"Last Name",
validators=[Required(), Alphabet()]
)
lname_request = StringField("Last Name", validators=[Required(), Alphabet()])
email_request = EmailField(
"Email Address",
validators=[Required(), Email()]
)
email_request = EmailField("Email Address", validators=[Required(), Email()])
phone_number = TelField(
"Phone Number",
validators=[Required(), PhoneNumber()]
)
phone_number = TelField("Phone Number", validators=[Required(), PhoneNumber()])
service_branch = StringField(
"Service Branch or Agency",
validators=[Required()]
)
service_branch = StringField("Service Branch or Agency", validators=[Required()])
citizenship = RadioField(
choices=[

View File

@ -6,22 +6,12 @@ from .validators import IsNumber, Alphabet
class POCForm(ValidatedForm):
fname_poc = StringField(
"POC First Name",
validators=[Required()]
)
fname_poc = StringField("POC First Name", validators=[Required()])
lname_poc = StringField(
"POC Last Name",
validators=[Required()]
)
lname_poc = StringField("POC Last Name", validators=[Required()])
email_poc = EmailField(
"POC Email Address",
validators=[Required(), Email()]
)
email_poc = EmailField("POC Email Address", validators=[Required(), Email()])
dodid_poc = StringField(
"DOD ID",
validators=[Required(), Length(min=10), IsNumber()]
"DOD ID", validators=[Required(), Length(min=10), IsNumber()]
)

View File

@ -14,18 +14,21 @@ class RequestForm(ValidatedForm):
"DoD Component",
description="Identify the DoD component that is requesting access to the JEDI Cloud",
choices=[
("null","Select an option"),
("us_air_force","US Air Force"),
("us_army","US Army"),
("us_navy","US Navy"),
("us_marine_corps","US Marine Corps"),
("joint_chiefs_of_staff","Joint Chiefs of Staff")],
("null", "Select an option"),
("us_air_force", "US Air Force"),
("us_army", "US Army"),
("us_navy", "US Navy"),
("us_marine_corps", "US Marine Corps"),
("joint_chiefs_of_staff", "Joint Chiefs of Staff"),
],
)
jedi_usage = TextAreaField(
"JEDI Usage",
description="Briefly describe how you are expecting to use the JEDI Cloud",
render_kw={"placeholder": "e.g. We are migrating XYZ application to the cloud so that..."},
render_kw={
"placeholder": "e.g. We are migrating XYZ application to the cloud so that..."
},
)
# Details of Use: Cloud Readiness
@ -41,7 +44,7 @@ class RequestForm(ValidatedForm):
rationalization_software_systems = RadioField(
"Have you completed a “rationalization” of your software systems to move to the cloud?",
choices=[("yes", "Yes"), ("no", "No"), ("in_progress","In Progress")],
choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
)
technical_support_team = RadioField(
@ -52,40 +55,43 @@ class RequestForm(ValidatedForm):
organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio
"If you are receiving migration assistance, indicate the type of organization providing assistance below:",
choices=[
("in_house_staff","In-house staff"),
("contractor","Contractor"),
("other_dod_organization","Other DoD organization")],
("in_house_staff", "In-house staff"),
("contractor", "Contractor"),
("other_dod_organization", "Other DoD organization"),
],
)
engineering_assessment = RadioField(
description="Have you completed an engineering assessment of your software systems for cloud readiness?",
choices=[("yes", "Yes"), ("no", "No"), ("in_progress","In Progress")],
choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
)
data_transfers = SelectField(
description="How much data is being transferred to the cloud?",
choices=[
("null","Select an option"),
("less_than_100gb","Less than 100GB"),
("100gb-500gb","100GB-500GB"),
("500gb-1tb","500GB-1TB"),
("1tb-50tb","1TB-50TB"),
("50tb-100tb","50TB-100TB"),
("100tb-500tb","100TB-500TB"),
("500tb-1pb","500TB-1PB"),
("1pb-5pb","1PB-5PB"),
("5pb-10pb","5PB-10PB"),
("above_10pb","Above 10PB")],
("null", "Select an option"),
("less_than_100gb", "Less than 100GB"),
("100gb-500gb", "100GB-500GB"),
("500gb-1tb", "500GB-1TB"),
("1tb-50tb", "1TB-50TB"),
("50tb-100tb", "50TB-100TB"),
("100tb-500tb", "100TB-500TB"),
("500tb-1pb", "500TB-1PB"),
("1pb-5pb", "1PB-5PB"),
("5pb-10pb", "5PB-10PB"),
("above_10pb", "Above 10PB"),
],
)
expected_completion_date = SelectField(
description="When do you expect to complete your migration to the JEDI Cloud?",
choices=[
("null","Select an option"),
("less_than_1_month","Less than 1 month"),
("1_to_3_months","1-3 months"),
("3_to_6_months","3-6 months"),
("above_12_months","Above 12 months")],
("null", "Select an option"),
("less_than_1_month", "Less than 1 month"),
("1_to_3_months", "1-3 months"),
("3_to_6_months", "3-6 months"),
("above_12_months", "Above 12 months"),
],
)
cloud_native = RadioField(
@ -96,7 +102,7 @@ class RequestForm(ValidatedForm):
# Details of Use: Financial Usage
estimated_monthly_spend = IntegerField(
"Estimated monthly spend",
description="Use the <a href=\"#\">JEDI CSP Calculator</a> to estimate your monthly cloud resource usage and enter the dollar amount below. Note these estimates are for initial approval only. After the request is approved, you will be asked to provide a valid Task Order number with specific CLIN amounts for cloud services."
description='Use the <a href="#">JEDI CSP Calculator</a> to estimate your monthly cloud resource usage and enter the dollar amount below. Note these estimates are for initial approval only. After the request is approved, you will be asked to provide a valid Task Order number with specific CLIN amounts for cloud services.',
)
dollar_value = IntegerField(
@ -105,17 +111,13 @@ class RequestForm(ValidatedForm):
)
number_user_sessions = IntegerField(
description="How many user sessions do you expect on these systems each day?",
description="How many user sessions do you expect on these systems each day?"
)
average_daily_traffic = IntegerField(
description="What is the average daily traffic you expect the systems under this cloud contract to use?",
description="What is the average daily traffic you expect the systems under this cloud contract to use?"
)
start_date = DateField(
description="When do you expect to start using the JEDI Cloud (not for billing purposes)?",
description="When do you expect to start using the JEDI Cloud (not for billing purposes)?"
)

View File

@ -1,51 +0,0 @@
import tornado.web
from atst.assets import environment
from atst.sessions import SessionNotFoundError
helpers = {"assets": environment}
class BaseHandler(tornado.web.RequestHandler):
def get_template_namespace(self):
ns = super(BaseHandler, self).get_template_namespace()
helpers["config"] = self.application.config
ns.update(helpers)
return ns
@tornado.gen.coroutine
def login(self, user):
user_permissions = yield self._get_user_permissions(user["id"])
user["atat_permissions"] = user_permissions["atat_permissions"]
user["atat_role"] = user_permissions["atat_role"]
session_id = self.sessions.start_session(user)
self.set_secure_cookie("atat", session_id)
return self.redirect("/home")
@tornado.gen.coroutine
def _get_user_permissions(self, user_id):
response = yield self.authz_client.get(
"/users/{}".format(user_id), raise_error=False
)
if response.code == 404:
response = yield self.authz_client.post(
"/users", json={"id": user_id, "atat_role": "developer"}
)
return response.json
else:
return response.json
def get_current_user(self):
cookie = self.get_secure_cookie("atat")
if cookie:
try:
session = self.application.sessions.get_session(cookie)
except SessionNotFoundError:
self.clear_cookie("atat")
return None
else:
return None
return session["user"]

View File

@ -1,64 +0,0 @@
import tornado.gen
from atst.handler import BaseHandler
_DEV_USERS = {
"sam": {
"id": "164497f6-c1ea-4f42-a5ef-101da278c012",
"first_name": "Sam",
"last_name": "Seeceepio",
"atat_role": "ccpo"
},
"amanda": {
"id": "cce17030-4109-4719-b958-ed109dbb87c8",
"first_name": "Amanda",
"last_name": "Adamson",
"atat_role": "default"
},
"brandon": {
"id": "66ebf7b8-cbf0-4ed8-a102-5f105330df75",
"first_name": "Brandon",
"last_name": "Buchannan",
"atat_role": "default"
},
"christina": {
"id": "7707b9f2-5945-49ae-967a-be65baa88baf",
"first_name": "Christina",
"last_name": "Collins",
"atat_role": "default"
},
"dominick": {
"id": "6978ac0c-442a-46aa-a0c3-ff17b5ec2a8c",
"first_name": "Dominick",
"last_name": "Domingo",
"atat_role": "default"
},
"erica": {
"id": "596fd001-bb1d-4adf-87d8-fa2312e882de",
"first_name": "Erica",
"last_name": "Eichner",
"atat_role": "default"
},
}
class Dev(BaseHandler):
def initialize(self, action, sessions, authz_client):
self.action = action
self.sessions = sessions
self.authz_client = authz_client
@tornado.gen.coroutine
def get(self):
role = self.get_argument("username", "amanda")
user = _DEV_USERS[role]
yield self._set_user_permissions(user["id"], user["atat_role"])
yield self.login(user)
@tornado.gen.coroutine
def _set_user_permissions(self, user_id, role):
response = yield self.authz_client.post(
"/users", json={"id": user_id, "atat_role": role}
)
return response.json

View File

@ -1,38 +0,0 @@
import tornado
from atst.handler import BaseHandler
class LoginRedirect(BaseHandler):
def initialize(self, authnid_client, sessions, authz_client):
self.authnid_client = authnid_client
self.sessions = sessions
self.authz_client = authz_client
@tornado.gen.coroutine
def get(self):
token = self.get_query_argument("bearer-token")
if token:
user = yield self._fetch_user_info(token)
if user:
yield self.login(user)
else:
self.write_error(401)
url = self.get_login_url()
self.redirect(url)
@tornado.gen.coroutine
def _fetch_user_info(self, token):
try:
response = yield self.authnid_client.post(
"/validate", json={"token": token}
)
if response.code == 200:
return response.json["user"]
except tornado.httpclient.HTTPError as error:
if error.response.code == 401:
return None
else:
raise error

View File

@ -1,11 +0,0 @@
import tornado
from atst.handler import BaseHandler
class Main(BaseHandler):
def initialize(self, page):
self.page = page
@tornado.web.authenticated
def get(self):
self.render("%s.html.to" % self.page, page=self.page)

View File

@ -1,43 +0,0 @@
import tornado
import pendulum
from atst.handler import BaseHandler
def map_request(user, request):
time_created = pendulum.parse(request["time_created"])
is_new = time_created.add(days=1) > pendulum.now()
return {
"order_id": request["id"],
"is_new": is_new,
"status": request["status"],
"app_count": 1,
"date": time_created.format("M/DD/YYYY"),
"full_name": "{} {}".format(user["first_name"], user["last_name"]),
}
class Request(BaseHandler):
def initialize(self, page, requests_client):
self.page = page
self.requests_client = requests_client
@tornado.web.authenticated
@tornado.gen.coroutine
def get(self):
user = self.get_current_user()
requests = yield self.fetch_requests(user)
mapped_requests = [map_request(user, request) for request in requests]
self.render("requests.html.to", page=self.page, requests=mapped_requests)
@tornado.gen.coroutine
def fetch_requests(self, user):
if "review_and_approve_jedi_workspace_request" in user["atat_permissions"]:
response = yield self.requests_client.get("/requests")
else:
response = yield self.requests_client.get(
"/requests?creator_id={}".format(user["id"])
)
return response.json["requests"]

View File

@ -1,75 +0,0 @@
import tornado
from atst.handler import BaseHandler
from atst.forms.financial import FinancialForm
class RequestFinancialVerification(BaseHandler):
def initialize(self, page, requests_client, fundz_client):
self.page = page
self.requests_client = requests_client
self.fundz_client = fundz_client
@tornado.gen.coroutine
def get_existing_request(self, request_id):
if request_id is None:
return {}
request = yield self.requests_client.get("/requests/{}".format(request_id))
return request.json
@tornado.web.authenticated
@tornado.gen.coroutine
def get(self, request_id=None):
existing_request = yield self.get_existing_request(request_id)
form = FinancialForm(data=existing_request['body'].get('financial_verification'))
self.render(
"requests/financial_verification.html.to",
page=self.page,
f=form,
request_id=request_id,
)
@tornado.gen.coroutine
def update_request(self, request_id, form_data):
request_data = {
"creator_id": self.current_user["id"],
"request": {"financial_verification": form_data},
}
response = yield self.requests_client.patch(
"/requests/{}".format(request_id), json=request_data
)
return response
@tornado.web.authenticated
@tornado.gen.coroutine
def post(self, request_id=None):
self.check_xsrf_cookie()
post_data = self.request.arguments
existing_request = yield self.get_existing_request(request_id)
form = FinancialForm(post_data)
rerender_args = dict(request_id=request_id, f=form)
if form.validate():
response = yield self.update_request(request_id, form.data)
if response.ok:
valid = yield form.perform_extra_validation(
existing_request.get('body', {}).get('financial_verification'),
self.fundz_client
)
if valid:
self.redirect(
self.application.default_router.reverse_url("financial_verification_submitted")
)
else:
self.render(
"requests/financial_verification.html.to",
**rerender_args
)
else:
self.set_status(response.code)
else:
self.render(
"requests/financial_verification.html.to",
**rerender_args
)

View File

@ -1,240 +0,0 @@
import tornado
from collections import defaultdict
from atst.handler import BaseHandler
from atst.forms.request import RequestForm
from atst.forms.org import OrgForm
from atst.forms.poc import POCForm
from atst.forms.review import ReviewForm
class RequestNew(BaseHandler):
def initialize(self, page, requests_client, fundz_client):
self.page = page
self.requests_client = requests_client
self.fundz_client = fundz_client
@tornado.gen.coroutine
def get_existing_request(self, request_id):
if request_id is None:
return {}
request = yield self.requests_client.get("/requests/{}".format(request_id))
return request.json
@tornado.web.authenticated
@tornado.gen.coroutine
def post(self, screen=1, request_id=None):
self.check_xsrf_cookie()
screen = int(screen)
post_data = self.request.arguments
current_user = self.get_current_user()
existing_request = yield self.get_existing_request(request_id)
jedi_flow = JEDIRequestFlow(
self.requests_client,
self.fundz_client,
screen,
post_data=post_data,
request_id=request_id,
current_user=current_user,
existing_request=existing_request,
)
rerender_args = dict(
f=jedi_flow.form,
data=post_data,
page=self.page,
screens=jedi_flow.screens,
current=screen,
next_screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
)
if jedi_flow.validate():
response = yield jedi_flow.create_or_update_request()
if response.ok:
valid = yield jedi_flow.validate_warnings()
if valid:
if jedi_flow.next_screen > len(jedi_flow.screens):
where = "/requests"
else:
where = self.application.default_router.reverse_url(
"request_form_update", jedi_flow.next_screen, jedi_flow.request_id
)
self.redirect(where)
else:
self.render(
"requests/screen-%d.html.to" % int(screen),
**rerender_args
)
else:
self.set_status(response.code)
else:
self.render(
"requests/screen-%d.html.to" % int(screen),
**rerender_args
)
@tornado.web.authenticated
@tornado.gen.coroutine
def get(self, screen=1, request_id=None):
screen = int(screen)
request = None
if request_id:
response = yield self.requests_client.get(
"/requests/{}".format(request_id),
raise_error=False,
)
if response.ok:
request = response.json
jedi_flow = JEDIRequestFlow(
self.requests_client, self.fundz_client, screen, request, request_id=request_id
)
self.render(
"requests/screen-%d.html.to" % int(screen),
f=jedi_flow.form,
data=jedi_flow.current_step_data,
page=self.page,
screens=jedi_flow.screens,
current=screen,
next_screen=screen + 1,
request_id=request_id,
can_submit=jedi_flow.can_submit
)
class JEDIRequestFlow(object):
def __init__(
self,
requests_client,
fundz_client,
current_step,
request=None,
post_data=None,
request_id=None,
current_user=None,
existing_request=None,
):
self.requests_client = requests_client
self.fundz_client = fundz_client
self.current_step = current_step
self.request = request
self.post_data = post_data
self.is_post = self.post_data is not None
self.request_id = request_id
self.form = self._form()
self.current_user = current_user
self.existing_request = existing_request
def _form(self):
if self.is_post:
return self.form_class()(self.post_data)
elif self.request:
return self.form_class()(data=self.current_step_data)
else:
return self.form_class()()
def validate(self):
return self.form.validate()
@tornado.gen.coroutine
def validate_warnings(self):
valid = yield self.form.perform_extra_validation(
self.existing_request.get('body', {}).get(self.form_section),
self.fundz_client,
)
return valid
@property
def current_screen(self):
return self.screens[self.current_step - 1]
@property
def form_section(self):
return self.current_screen["section"]
def form_class(self):
return self.current_screen["form"]
@property
def current_step_data(self):
data = {}
if self.is_post:
data = self.post_data
if self.request:
if self.form_section == "review_submit":
data = self.request["body"]
else:
data = self.request["body"].get(self.form_section, {})
return defaultdict(lambda: defaultdict(lambda: 'Input required'), data)
@property
def can_submit(self):
return self.request and self.request["status"] != "incomplete"
@property
def next_screen(self):
return self.current_step + 1
@property
def screens(self):
return [
{
"title": "Details of Use",
"section": "details_of_use",
"form": RequestForm,
"subitems": [
{
"title": "Overall request details",
"id": "overall-request-details",
},
{"title": "Cloud Resources", "id": "cloud-resources"},
{"title": "Support Staff", "id": "support-staff"},
],
"show": True,
},
{
"title": "Information About You",
"section": "information_about_you",
"form": OrgForm,
"show": True,
},
{
"title": "Primary Point of Contact",
"section": "primary_poc",
"form": POCForm,
"show": True,
},
{
"title": "Review & Submit",
"section": "review_submit",
"form": ReviewForm,
"show":True,
},
]
@tornado.gen.coroutine
def create_or_update_request(self):
request_data = {
"creator_id": self.current_user["id"],
"request": {self.form_section: self.form.data},
}
if self.request_id:
response = yield self.requests_client.patch(
"/requests/{}".format(self.request_id), json=request_data
)
else:
response = yield self.requests_client.post("/requests", json=request_data)
self.request = response.json
self.request_id = self.request["id"]
return response

View File

@ -1,29 +0,0 @@
import tornado
from atst.handler import BaseHandler
class RequestsSubmit(BaseHandler):
def initialize(self, requests_client):
self.requests_client = requests_client
@tornado.web.authenticated
@tornado.gen.coroutine
def post(self, request_id):
yield self.requests_client.post(
"/requests/{}/submit".format(request_id),
allow_nonstandard_methods=True
)
approved = yield self._check_approved(request_id)
if approved:
self.redirect("/requests?modal=True")
else:
self.redirect("/requests")
@tornado.gen.coroutine
def _check_approved(self, request_id):
response = yield self.requests_client.get(
"/requests/{}".format(request_id)
)
status = response.json.get("status")
return status == "approved"

View File

@ -1,9 +0,0 @@
from atst.handler import BaseHandler
class Root(BaseHandler):
def initialize(self, page):
self.page = page
def get(self):
self.render("%s.html.to" % self.page, page=self.page)

View File

@ -1,15 +0,0 @@
import tornado
from atst.handler import BaseHandler
from atst.domain.workspaces import Projects
class Workspace(BaseHandler):
def initialize(self):
self.projects_repo = Projects()
@tornado.web.authenticated
@tornado.gen.coroutine
def get(self, workspace_id):
projects = self.projects_repo.get_many(workspace_id)
self.render("workspace_projects.html.to", workspace_id=workspace_id, projects=projects)

View File

@ -1,15 +0,0 @@
import tornado
from atst.handler import BaseHandler
from atst.domain.workspaces import Members
class WorkspaceMembers(BaseHandler):
def initialize(self):
self.members_repo = Members()
@tornado.web.authenticated
@tornado.gen.coroutine
def get(self, workspace_id):
members = self.members_repo.get_many(workspace_id)
self.render("workspace_members.html.to", workspace_id=workspace_id, members=members)

View File

@ -1,22 +0,0 @@
from atst.handler import BaseHandler
import tornado
mock_workspaces = [
{
"name": "Unclassified IaaS and PaaS for Defense Digital Service (DDS)",
"id": "5966187a-eff9-44c3-aa15-4de7a65ac7ff",
"task_order": {"number": 123456},
"user_count": 23,
}
]
class Workspaces(BaseHandler):
def initialize(self, page, authz_client):
self.page = page
self.authz_client = authz_client
@tornado.gen.coroutine
@tornado.web.authenticated
def get(self):
self.render("workspaces.html.to", page=self.page, workspaces=mock_workspaces)

View File

@ -1,3 +0,0 @@
from unipath import Path
home = Path(__file__).parent.parent

12
atst/models/__init__.py Normal file
View File

@ -0,0 +1,12 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
from .request import Request
from .request_status_event import RequestStatusEvent
from .permissions import Permissions
from .role import Role
from .user import User
from .workspace_role import WorkspaceRole
from .pe_number import PENumber
from .task_order import TaskOrder

15
atst/models/pe_number.py Normal file
View File

@ -0,0 +1,15 @@
from sqlalchemy import String, Column
from atst.models import Base
class PENumber(Base):
__tablename__ = "pe_number"
number = Column(String, primary_key=True)
description = Column(String)
def __repr__(self):
return "<PENumber(number='{}', description='{}')>".format(
self.number, self.description
)

View File

@ -0,0 +1,40 @@
class Permissions(object):
REQUEST_JEDI_WORKSPACE = "request_jedi_workspace"
VIEW_ORIGINAL_JEDI_REQEUST = "view_original_jedi_request"
REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST = (
"review_and_approve_jedi_workspace_request"
)
MODIFY_ATAT_ROLE_PERMISSIONS = "modify_atat_role_permissions"
CREATE_CSP_ROLE = "create_csp_role"
DELETE_CSP_ROLE = "delete_csp_role"
DEACTIVE_CSP_ROLE = "deactivate_csp_role"
MODIFY_CSP_ROLE_PERMISSIONS = "modify_csp_role_permissions"
VIEW_USAGE_REPORT = "view_usage_report"
VIEW_USAGE_DOLLARS = "view_usage_dollars"
ADD_AND_ASSIGN_CSP_ROLES = "add_and_assign_csp_roles"
REMOVE_CSP_ROLES = "remove_csp_roles"
REQUEST_NEW_CSP_ROLE = "request_new_csp_role"
ASSIGN_AND_UNASSIGN_ATAT_ROLE = "assign_and_unassign_atat_role"
VIEW_ASSIGNED_ATAT_ROLE_CONFIGURATIONS = "view_assigned_atat_role_configurations"
VIEW_ASSIGNED_CSP_ROLE_CONFIGURATIONS = "view_assigned_csp_role_configurations"
DEACTIVATE_WORKSPACE = "deactivate_workspace"
VIEW_ATAT_PERMISSIONS = "view_atat_permissions"
TRANSFER_OWNERSHIP_OF_WORKSPACE = "transfer_ownership_of_workspace"
ADD_APPLICATION_IN_WORKSPACE = "add_application_in_workspace"
DELETE_APPLICATION_IN_WORKSPACE = "delete_application_in_workspace"
DEACTIVATE_APPLICATION_IN_WORKSPACE = "deactivate_application_in_workspace"
VIEW_APPLICATION_IN_WORKSPACE = "view_application_in_workspace"
RENAME_APPLICATION_IN_WORKSPACE = "rename_application_in_workspace"
ADD_ENVIRONMENT_IN_APPLICATION = "add_environment_in_application"
DELETE_ENVIRONMENT_IN_APPLICATION = "delete_environment_in_application"
DEACTIVATE_ENVIRONMENT_IN_APPLICATION = "deactivate_environment_in_application"
VIEW_ENVIRONMENT_IN_APPLICATION = "view_environment_in_application"
RENAME_ENVIRONMENT_IN_APPLICATION = "rename_environment_in_application"
ADD_TAG_TO_WORKSPACE = "add_tag_to_workspace"
REMOVE_TAG_FROM_WORKSPACE = "remove_tag_from_workspace"

32
atst/models/request.py Normal file
View File

@ -0,0 +1,32 @@
from sqlalchemy import Column, func
from sqlalchemy.types import DateTime
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import relationship
from atst.models import Base
from atst.models.types import Id
class Request(Base):
__tablename__ = "requests"
id = Id()
creator = Column(UUID(as_uuid=True))
time_created = Column(DateTime(timezone=True), server_default=func.now())
body = Column(JSONB)
status_events = relationship(
"RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence"
)
@property
def status(self):
return self.status_events[-1].new_status
@property
def action_required_by(self):
return {
"incomplete": "mission_owner",
"pending_submission": "mission_owner",
"submitted": "ccpo",
"approved": "mission_owner",
}.get(self.status)

View File

@ -0,0 +1,21 @@
from sqlalchemy import Column, func, ForeignKey
from sqlalchemy.types import DateTime, String, BigInteger
from sqlalchemy.schema import Sequence
from sqlalchemy.dialects.postgresql import UUID
from atst.models import Base
from atst.models.types import Id
class RequestStatusEvent(Base):
__tablename__ = "request_status_events"
id = Id()
new_status = Column(String())
time_created = Column(DateTime(timezone=True), server_default=func.now())
request_id = Column(
UUID(as_uuid=True), ForeignKey("requests.id", ondelete="CASCADE")
)
sequence = Column(
BigInteger, Sequence("request_status_events_sequence_seq"), nullable=False
)

14
atst/models/role.py Normal file
View File

@ -0,0 +1,14 @@
from sqlalchemy import String, Column
from sqlalchemy.dialects.postgresql import ARRAY
from atst.models import Base
from .types import Id
class Role(Base):
__tablename__ = "roles"
id = Id()
name = Column(String, index=True, unique=True)
description = Column(String)
permissions = Column(ARRAY(String), index=True, server_default="{}")

10
atst/models/task_order.py Normal file
View File

@ -0,0 +1,10 @@
from sqlalchemy import Column, Integer, String
from atst.models import Base
class TaskOrder(Base):
__tablename__ = "task_order"
id = Column(Integer, primary_key=True)
number = Column(String)

11
atst/models/types.py Normal file
View File

@ -0,0 +1,11 @@
import sqlalchemy
from sqlalchemy import Column
from sqlalchemy.dialects.postgresql import UUID
def Id():
return Column(
UUID(as_uuid=True),
primary_key=True,
server_default=sqlalchemy.text("uuid_generate_v4()"),
)

30
atst/models/user.py Normal file
View File

@ -0,0 +1,30 @@
from sqlalchemy import String, ForeignKey, Column
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
from atst.models import Base
from .types import Id
class User(Base):
__tablename__ = "users"
id = Id()
username = Column(String)
atat_role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"))
atat_role = relationship("Role")
workspace_roles = relationship("WorkspaceRole", backref="user")
email = Column(String, unique=True)
dod_id = Column(String, unique=True)
first_name = Column(String)
last_name = Column(String)
@property
def atat_permissions(self):
return self.atat_role.permissions
@property
def full_name(self):
return "{} {}".format(self.first_name, self.last_name)

View File

@ -0,0 +1,24 @@
from sqlalchemy import Index, ForeignKey, Column
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from atst.models import Base
from .types import Id
class WorkspaceRole(Base):
__tablename__ = "workspace_role"
id = Id()
workspace_id = Column(UUID(as_uuid=True), index=True)
role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"))
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
role = relationship("Role")
Index(
"workspace_role_user_workspace",
WorkspaceRole.user_id,
WorkspaceRole.workspace_id,
unique=True,
)

View File

@ -0,0 +1,14 @@
class WorkspaceUser(object):
def __init__(self, user, workspace_role):
self.user = user
self.workspace_role = workspace_role
def permissions(self):
atat_permissions = set(self.user.atat_role.permissions)
workspace_permissions = (
[] if self.workspace_role is None else self.workspace_role.role.permissions
)
return set(workspace_permissions).union(atat_permissions)
def workspace_id(self):
return self.workspace_role.workspace_id

61
atst/routes/__init__.py Normal file
View File

@ -0,0 +1,61 @@
from flask import Blueprint, render_template, g, redirect, session, url_for, request
from flask import current_app as app
import pendulum
from atst.domain.requests import Requests
from atst.domain.users import Users
from atst.domain.authnid.utils import parse_sdn
bp = Blueprint("atst", __name__)
@bp.route("/")
def root():
return render_template("root.html")
@bp.route("/home")
def home():
return render_template("home.html")
@bp.route("/styleguide")
def styleguide():
return render_template("styleguide.html")
@bp.route('/<path:path>')
def catch_all(path):
return render_template("{}.html".format(path))
@bp.route('/login-redirect')
def login_redirect():
if request.environ.get('HTTP_X_SSL_CLIENT_VERIFY') == 'SUCCESS' and _is_valid_certificate(request):
sdn = request.environ.get('HTTP_X_SSL_CLIENT_S_DN')
sdn_parts = parse_sdn(sdn)
user = Users.get_or_create_by_dod_id(**sdn_parts)
session["user_id"] = user.id
return redirect(url_for("atst.home"))
else:
return redirect(url_for("atst.unauthorized"))
@bp.route("/unauthorized")
def unauthorized():
template = render_template('unauthorized.html')
response = app.make_response(template)
response.status_code = 401
return response
def _is_valid_certificate(request):
cert = request.environ.get('HTTP_X_SSL_CLIENT_CERT')
if cert:
result = app.crl_validator.validate(cert.encode())
if not result:
app.logger.info(app.crl_validator.errors[-1])
return result
else:
return False

58
atst/routes/dev.py Normal file
View File

@ -0,0 +1,58 @@
from flask import Blueprint, request, session, redirect, url_for
from atst.domain.users import Users
bp = Blueprint("dev", __name__)
_DEV_USERS = {
"sam": {
"dod_id": "1234567890",
"first_name": "Sam",
"last_name": "Seeceepio",
"atat_role": "ccpo",
},
"amanda": {
"dod_id": "2345678901",
"first_name": "Amanda",
"last_name": "Adamson",
"atat_role": "default",
},
"brandon": {
"dod_id": "3456789012",
"first_name": "Brandon",
"last_name": "Buchannan",
"atat_role": "default",
},
"christina": {
"dod_id": "4567890123",
"first_name": "Christina",
"last_name": "Collins",
"atat_role": "default",
},
"dominick": {
"dod_id": "5678901234",
"first_name": "Dominick",
"last_name": "Domingo",
"atat_role": "default",
},
"erica": {
"dod_id": "6789012345",
"first_name": "Erica",
"last_name": "Eichner",
"atat_role": "default",
},
}
@bp.route("/login-dev")
def login_dev():
role = request.args.get("username", "amanda")
user_data = _DEV_USERS[role]
basic_data = {k:v for k,v in user_data.items() if k not in ["dod_id", "atat_role"]}
user = _set_user_permissions(user_data["dod_id"], user_data["atat_role"], basic_data)
session["user_id"] = user.id
return redirect(url_for("atst.home"))
def _set_user_permissions(dod_id, role, user_data):
return Users.get_or_create_by_dod_id(dod_id, atat_role_name=role, **user_data)

View File

@ -0,0 +1,7 @@
from flask import Blueprint
requests_bp = Blueprint("requests", __name__)
from . import index
from . import requests_form
from . import financial_verification

View File

@ -0,0 +1,44 @@
from flask import render_template, redirect, url_for
from flask import request as http_request
from . import requests_bp
from atst.domain.requests import Requests
from atst.forms.financial import FinancialForm
@requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"])
def financial_verification(request_id=None):
request = Requests.get(request_id)
form = FinancialForm(data=request.body.get("financial_verification"))
return render_template(
"requests/financial_verification.html", f=form, request_id=request_id
)
@requests_bp.route("/requests/verify/<string:request_id>", methods=["POST"])
def update_financial_verification(request_id):
post_data = http_request.form
existing_request = Requests.get(request_id)
form = FinancialForm(post_data)
rerender_args = dict(request_id=request_id, f=form)
if form.validate():
request_data = {"financial_verification": post_data}
Requests.update(request_id, request_data)
valid = form.perform_extra_validation(
existing_request.body.get("financial_verification")
)
if valid:
return redirect(url_for("requests.financial_verification_submitted"))
else:
return render_template(
"requests/financial_verification.html", **rerender_args
)
else:
return render_template("requests/financial_verification.html", **rerender_args)
@requests_bp.route("/requests/financial_verification_submitted")
def financial_verification_submitted():
pass

View File

@ -0,0 +1,35 @@
import pendulum
from flask import render_template, g
from . import requests_bp
from atst.domain.requests import Requests
def map_request(user, request):
time_created = pendulum.instance(request.time_created)
is_new = time_created.add(days=1) > pendulum.now()
return {
"order_id": request.id,
"is_new": is_new,
"status": request.status,
"app_count": 1,
"date": time_created.format("M/DD/YYYY"),
"full_name": user.full_name
}
@requests_bp.route("/requests", methods=["GET"])
def requests_index():
requests = []
if (
"review_and_approve_jedi_workspace_request"
in g.current_user.atat_permissions
):
requests = Requests.get_many()
else:
requests = Requests.get_many(creator_id=g.current_user.id)
mapped_requests = [map_request(g.current_user, r) for r in requests]
return render_template("requests.html", requests=mapped_requests)

View File

@ -0,0 +1,128 @@
from collections import defaultdict
from atst.domain.requests import Requests
from atst.forms.request import RequestForm
from atst.forms.org import OrgForm
from atst.forms.poc import POCForm
from atst.forms.review import ReviewForm
class JEDIRequestFlow(object):
def __init__(
self,
current_step,
request=None,
post_data=None,
request_id=None,
current_user=None,
existing_request=None,
):
self.current_step = current_step
self.request = request
self.post_data = post_data
self.is_post = self.post_data is not None
self.request_id = request_id
self.form = self._form()
self.current_user = current_user
self.existing_request = existing_request
def _form(self):
if self.is_post:
return self.form_class()(self.post_data)
elif self.request:
return self.form_class()(data=self.current_step_data)
else:
return self.form_class()()
def validate(self):
return self.form.validate()
def validate_warnings(self):
existing_request_data = (
self.existing_request and self.existing_request.body.get(self.form_section)
) or None
valid = self.form.perform_extra_validation(existing_request_data)
return valid
@property
def current_screen(self):
return self.screens[self.current_step - 1]
@property
def form_section(self):
return self.current_screen["section"]
def form_class(self):
return self.current_screen["form"]
@property
def current_step_data(self):
data = {}
if self.is_post:
data = self.post_data
if self.request:
if self.form_section == "review_submit":
data = self.request.body
else:
data = self.request.body.get(self.form_section, {})
return defaultdict(lambda: defaultdict(lambda: "Input required"), data)
@property
def can_submit(self):
return self.request and self.request.status != "incomplete"
@property
def next_screen(self):
return self.current_step + 1
@property
def screens(self):
return [
{
"title": "Details of Use",
"section": "details_of_use",
"form": RequestForm,
"subitems": [
{
"title": "Overall request details",
"id": "overall-request-details",
},
{"title": "Cloud Resources", "id": "cloud-resources"},
{"title": "Support Staff", "id": "support-staff"},
],
"show": True,
},
{
"title": "Information About You",
"section": "information_about_you",
"form": OrgForm,
"show": True,
},
{
"title": "Primary Point of Contact",
"section": "primary_poc",
"form": POCForm,
"show": True,
},
{
"title": "Review & Submit",
"section": "review_submit",
"form": ReviewForm,
"show": True,
},
]
def create_or_update_request(self):
request_data = {self.form_section: self.form.data}
if self.request_id:
Requests.update(self.request_id, request_data)
else:
request = Requests.create(self.current_user.id, request_data)
self.request_id = request.id

View File

@ -0,0 +1,98 @@
from flask import g, redirect, render_template, url_for, request as http_request
from . import requests_bp
from atst.domain.requests import Requests
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
def requests_form_new(screen):
jedi_flow = JEDIRequestFlow(screen, request=None)
return render_template(
"requests/screen-%d.html" % int(screen),
f=jedi_flow.form,
data=jedi_flow.current_step_data,
screens=jedi_flow.screens,
current=screen,
next_screen=screen + 1,
can_submit=jedi_flow.can_submit,
)
@requests_bp.route(
"/requests/new/<int:screen>", methods=["GET"], defaults={"request_id": None}
)
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["GET"])
def requests_form_update(screen=1, request_id=None):
request = Requests.get(request_id) if request_id is not None else None
jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id)
return render_template(
"requests/screen-%d.html" % int(screen),
f=jedi_flow.form,
data=jedi_flow.current_step_data,
screens=jedi_flow.screens,
current=screen,
next_screen=screen + 1,
request_id=request_id,
can_submit=jedi_flow.can_submit,
)
@requests_bp.route(
"/requests/new/<int:screen>", methods=["POST"], defaults={"request_id": None}
)
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["POST"])
def requests_update(screen=1, request_id=None):
screen = int(screen)
post_data = http_request.form
current_user = g.current_user
existing_request = Requests.get(request_id) if request_id is not None else None
jedi_flow = JEDIRequestFlow(
screen,
post_data=post_data,
request_id=request_id,
current_user=current_user,
existing_request=existing_request,
)
rerender_args = dict(
f=jedi_flow.form,
data=post_data,
screens=jedi_flow.screens,
current=screen,
next_screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
)
if jedi_flow.validate():
jedi_flow.create_or_update_request()
valid = jedi_flow.validate_warnings()
if valid:
if jedi_flow.next_screen > len(jedi_flow.screens):
where = "/requests"
else:
where = url_for(
"requests.requests_form_update",
screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
)
return redirect(where)
else:
return render_template(
"requests/screen-%d.html" % int(screen), **rerender_args
)
else:
return render_template("requests/screen-%d.html" % int(screen), **rerender_args)
@requests_bp.route("/requests/submit/<string:request_id>", methods=["POST"])
def requests_submit(request_id=None):
request = Requests.get(request_id)
Requests.submit(request)
if request.status == "approved":
return redirect("/requests?modal=True")
else:
return redirect("/requests")

38
atst/routes/workspaces.py Normal file
View File

@ -0,0 +1,38 @@
from flask import Blueprint, render_template
from atst.domain.workspaces import Projects, Members
bp = Blueprint("workspaces", __name__)
mock_workspaces = [
{
"name": "Unclassified IaaS and PaaS for Defense Digital Service (DDS)",
"id": "5966187a-eff9-44c3-aa15-4de7a65ac7ff",
"task_order": {"number": 123456},
"user_count": 23,
}
]
@bp.route("/workspaces")
def workspaces():
return render_template("workspaces.html", page=5, workspaces=mock_workspaces)
@bp.route("/workspaces/<workspace_id>/projects")
def workspace_projects(workspace_id):
projects_repo = Projects()
projects = projects_repo.get_many(workspace_id)
return render_template(
"workspace_projects.html", workspace_id=workspace_id, projects=projects
)
@bp.route("/workspaces/<workspace_id>/members")
def workspace_members(workspace_id):
members_repo = Members()
members = members_repo.get_many(workspace_id)
return render_template(
"workspace_members.html", workspace_id=workspace_id, members=members
)

View File

@ -1,71 +0,0 @@
from uuid import uuid4
import json
from redis import exceptions
class SessionStorageError(Exception):
pass
class SessionNotFoundError(Exception):
pass
class Sessions(object):
def start_session(self, user):
raise NotImplementedError()
def get_session(self, session_id):
raise NotImplementedError()
def generate_session_id(self):
return str(uuid4())
def build_session_dict(self, user=None):
return {"user": user or {}}
class DictSessions(Sessions):
def __init__(self):
self.sessions = {}
def start_session(self, user):
session_id = self.generate_session_id()
self.sessions[session_id] = self.build_session_dict(user=user)
return session_id
def get_session(self, session_id):
try:
session = self.sessions[session_id]
except KeyError:
raise SessionNotFoundError
return session
class RedisSessions(Sessions):
def __init__(self, redis, ttl_seconds):
self.redis = redis
self.ttl_seconds = ttl_seconds
def start_session(self, user):
session_id = self.generate_session_id()
session_dict = self.build_session_dict(user=user)
session_serialized = json.dumps(session_dict)
try:
self.redis.setex(session_id, self.ttl_seconds, session_serialized)
except exceptions.ConnectionError:
raise SessionStorageError
return session_id
def get_session(self, session_id):
try:
session_serialized = self.redis.get(session_id)
except exceptions.ConnectionError:
raise
if session_serialized:
self.redis.expire(session_id, self.ttl_seconds)
return json.loads(session_serialized)
else:
raise SessionNotFoundError

View File

@ -1,20 +0,0 @@
import os
import re
def navigationContext(self):
return 'workspace' if re.match('\/workspaces\/[A-Za-z0-9]*', self.request.uri) else 'global'
def dev(self):
return os.getenv("TORNADO_ENV", "dev") == "dev"
def matchesPath(self, href):
return re.match('^'+href, self.request.uri)
def modal(self, body):
return self.render_string(
"components/modal.html.to",
body=body)
def modalOpen(self):
# For now, just check a dummy URL param
return self.get_argument("modal", False)

View File

@ -1,57 +0,0 @@
from tornado.web import UIModule
# from tornado.template import raw
import re
class Alert(UIModule):
def render(self, title, message=None, actions=None, level='info'):
return self.render_string(
"components/alert.html.to",
title=title,
message=message,
actions=actions,
level=level)
class TextInput(UIModule):
def render(self, field, placeholder=''):
return self.render_string(
"components/text_input.html.to",
field=field,
label=re.sub('<[^<]+?>', '', str(field.label)),
errors=field.errors,
placeholder=placeholder,
description=field.description)
class OptionsInput(UIModule):
def render(self, field, inline=False):
return self.render_string(
"components/options_input.html.to",
field=field,
label=re.sub('<[^<]+?>', '', str(field.label)),
errors=field.errors,
description=field.description,
inline=inline)
class Icon(UIModule):
def render(self, name, classes=''):
with open('static/icons/%s.svg' % name) as svg:
return self.render_string(
"components/icon.html.to", svg=svg.read(), name=name, classes=classes)
class SidenavItem(UIModule):
def render(self, label, href, active=False, icon=None, subnav=None):
return self.render_string(
"navigation/_sidenav_item.html.to",
label=label,
href=href,
active=active,
icon=icon,
subnav=subnav)
class EmptyState(UIModule):
def render(self, message, actionLabel, actionHref, icon=None):
return self.render_string(
"components/empty_state.html.to",
message=message,
actionLabel=actionLabel,
actionHref=actionHref,
icon=icon)

View File

@ -2,12 +2,22 @@
PORT=8000
ENVIRONMENT = dev
DEBUG = true
AUTHZ_BASE_URL = http://localhost:8002
AUTHNID_BASE_URL= https://localhost:8001
FUNDZ_BASE_URL= http://localhost:8004
COOKIE_SECRET = some-secret-please-replace
SECRET = change_me_into_something_secret
CAC_URL = https://localhost:8001
REQUESTS_QUEUE_BASE_URL = http://localhost:8003
SECRET_KEY = change_me_into_something_secret
CAC_URL = http://localhost:8000/login-redirect
PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv
REDIS_URI = redis://localhost:6379
SESSION_TTL_SECONDS = 600
PGAPPNAME = atst
PGHOST = localhost
PGPORT = 5432
PGUSER = postgres
PGPASSWORD = postgres
PGDATABASE = atat
SESSION_TYPE = redis
SESSION_COOKIE_NAME=atat
SESSION_USE_SIGNER = True
PERMANENT_SESSION_LIFETIME = 600
CRL_DIRECTORY = crl
CA_CHAIN = ssl/server-certs/ca-chain.pem
WTF_CSRF_ENABLED = true

View File

@ -0,0 +1,6 @@
[default]
PGHOST = postgreshost
PGDATABASE = atat_test
REDIS_URI = redis://redishost:6379
CRL_DIRECTORY = tests/fixtures/crl
WTF_CSRF_ENABLED = false

2
config/prod.ini Normal file
View File

@ -0,0 +1,2 @@
[default]
SESSION_COOKIE_SECURE=True

4
config/test.ini Normal file
View File

@ -0,0 +1,4 @@
[default]
PGDATABASE = atat_test
CRL_DIRECTORY = tests/fixtures/crl
WTF_CSRF_ENABLED = false

View File

@ -8,6 +8,7 @@ ARG APP_DIR=/opt/atat/atst
ARG CIBUILD=true
ENV APP_DIR "${APP_DIR}"
ENV FLASK_ENV ci
ENV SKIP_PIPENV true
# Use dumb-init for proper signal handling

9
ipython_setup.py Normal file
View File

@ -0,0 +1,9 @@
from atst.app import make_config, make_app
from atst.database import db
from atst.models import *
app = make_app(make_config())
ctx = app.app_context()
ctx.push()
print("\nWelcome to atst. This shell has all models in scope, and a SQLAlchemy session called db.")

4
js/example.js Normal file
View File

@ -0,0 +1,4 @@
window.onload = function() {
console.log('hello from example')
}

5
js/index.js Normal file
View File

@ -0,0 +1,5 @@
import classes from '../styles/atat.scss'
import './example'
console.log('hello from index')

View File

@ -4,12 +4,19 @@
"description": "ATST Stateless Services",
"main": "index.js",
"scripts": {
"watch": "parcel watch js/index.js -d static/assets --public-url /static/assets -o index.js --no-autoinstall",
"build": "parcel build js/index.js -d static/assets --public-url /static/assets -o index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"npm": "^6.0.1",
"uswds": "^1.6.3"
"parcel": "^1.9.7",
"uswds": "^1.6.3",
"vue": "^2.5.17"
},
"devDependencies": {
"node-sass": "^4.9.2"
}
}

View File

@ -5,9 +5,6 @@
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
# Set sass compiling command for this app
COMPILE_SASS_CMD="webassets -m atst.assets build"
# Enable python and node package installation
INSTALL_PYTHON_PACKAGES="true"
INSTALL_NODE_PACKAGES="true"

View File

@ -5,4 +5,19 @@
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
# Run lint/style checks and unit tests
source ./script/test
export FLASK_ENV=ci
# Enable database resetting
RESET_DB="true"
# Define all relevant python files and directories for this app
PYTHON_FILES="./app.py ./atst ./config"
# Enable Python testing
RUN_PYTHON_TESTS="true"
# Compile assets
yarn build
# Run the shared test script
source ./script/include/run_test

9
script/console Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
# If a command fails, exit the script
set -e
# Ensure we are in the app root directory (not the /script directory)
cd "$(dirname "${0}")/.."
pipenv run ipython -i ./ipython_setup.py

8
script/get_db_settings Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
# script/get_db_settings: Fetch postgresql settings and set them as ENV vars
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
# Run the shared get_db_settings script
source ./script/include/run_get_db_settings.sh

@ -1 +1 @@
Subproject commit 7417942f1614d6a7ad94e94d1621dca9b422dec2
Subproject commit 8cf96c9776e7fd73c11d57160d26fc1715bf00da

View File

@ -0,0 +1,33 @@
from urllib.request import urlopen
import csv
# Add root project dir to the python path
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.append(parent_dir)
from atst.app import make_app, make_config
from atst.domain.pe_numbers import PENumbers
def get_pe_numbers(url):
response = urlopen(url)
t = response.read().decode("utf-8")
return list(csv.reader(t.split("\r\n")))
def make_pe_number_repo(config):
deps = make_deps(config)
db = deps["db_session"]
return PENumbers(db)
if __name__ == "__main__":
config = make_config()
url = config['PE_NUMBER_CSV_URL']
print("Fetching PE numbers from {}".format(url))
pe_numbers = get_pe_numbers(url)
app = make_app(config)
with app.app_context():
print("Inserting {} PE numbers".format(len(pe_numbers)))
PENumbers.create_many(pe_numbers)

View File

@ -4,5 +4,8 @@
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
# Compile js/css assets
yarn build
# Launch the app
run_command "./app.py ${LAUNCH_ARGS}"

View File

@ -8,5 +8,11 @@ source "$(dirname "${0}")"/../script/include/global_header.inc.sh
# Turn on sass compiler installation
INSTALL_SASS="true"
# Enable database resetting
RESET_DB="true"
# Run the shared setup script
source ./script/include/run_setup
# Fetch and import the PE numbers
run_command "python script/ingest_pe_numbers.py"

16
script/sync-crls Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# script/sync-crls: update the DOD CRLs and place them where authnid expects them
set -e
cd "$(dirname "$0")/.."
mkdir -p crl-tmp
pipenv run python ./atst/domain/authnid/crl/util.py crl-tmp
mkdir -p crl
rsync -rq crl-tmp/. crl/.
rm -rf crl-tmp
if [[ $FLASK_ENV != "production" ]]; then
# place our test CRL there
cp ssl/client-certs/client-ca.der.crl crl/
fi

25
script/sync-dod-certs Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# script/sync-dod-certs: update the CA bundle with DOD intermediate and root CAs
CAS_FILE_NAME="Certificates_PKCS7_v5.3_DoD"
CA_CHAIN="ssl/server-certs/ca-chain.pem"
echo "Resetting CA bundle..."
rm ssl/server-certs/ca-chain.pem &> /dev/null || true
touch $CA_CHAIN
if [[ $FLASK_ENV != "production" ]]; then
# only for testing and development
echo "Copy in testing client CA..."
cat ssl/client-certs/client-ca.crt >> $CA_CHAIN
fi
# dod intermediate certs
echo "Adding DoD root certs"
rm -rf tmp || true
mkdir tmp
curl --silent -o tmp/dod-cas.zip "https://iasecontent.disa.mil/pki-pke/$CAS_FILE_NAME.zip"
unzip tmp/dod-cas.zip -d tmp/ &> /dev/null
openssl pkcs7 -in "tmp/$CAS_FILE_NAME/$CAS_FILE_NAME.pem.p7b" -print_certs >> $CA_CHAIN
rm -rf tmp

View File

@ -4,6 +4,11 @@
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
export FLASK_ENV=test
# Enable database resetting
RESET_DB="true"
# Define all relevant python files and directories for this app
PYTHON_FILES="./app.py ./atst ./config"

View File

@ -4,5 +4,11 @@
source "$(dirname "${0}")"/../script/include/global_header.inc.sh
# Run the bootstrap script
source ./script/bootstrap
# Enable DB migration
MIGRATE_DB="true"
# Run the shared update script
source ./script/include/run_update
# Fetch and import/update the PE numbers
run_command "python script/ingest_pe_numbers.py"

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEJDCCAwygAwIBAgIJAK4JGo3BBGhVMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
BAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFuaWExFTATBgNVBAcTDFBoaWxhZGVs
cGhpYTEMMAoGA1UEChMDRG9EMQwwCgYDVQQLEwNERFMxEDAOBgNVBAMTB0FUQVQg
Q0EwHhcNMTgwNjAxMTk0NjIyWhcNMzgwNTI3MTk0NjIyWjBpMQswCQYDVQQGEwJV
UzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRUwEwYDVQQHEwxQaGlsYWRlbHBoaWEx
DDAKBgNVBAoTA0RvRDEMMAoGA1UECxMDRERTMRAwDgYDVQQDEwdBVEFUIENBMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzYU7UbstArnnVliaC/TB6Vir
kVWMnAEYMUZA1BKP8DZaNEKbzFH2+mMw7O0BY7Ph9x0hEZ1kXLr6U93xcKyUWNPo
13i5EwUUCSh2MdPfS8ZZt8DUIIKC7XzFnKyKSKQmr0Mt9dC44rryPKTBvmI60rQ8
VZkFEgvs8FCP0M4Ar6/gtJ24ZLEtilu5dQBSlru4nPGXg07r2C2JgEZWshtMBtbH
LkOM2gtp/pkYCCG0zqeU+0s3H8IqDq0uYkONOfVeCumbg1/AtjgrZu7aOVPKyibk
aI6sTTooXE5aSZkfkx0z6+fKM2nPSe30HgiBODtb7G+44ln08d0isjpQ67OvGQID
AQABo4HOMIHLMB0GA1UdDgQWBBSl7CUAWPbx8XqotKKKAufPh0wn4DCBmwYDVR0j
BIGTMIGQgBSl7CUAWPbx8XqotKKKAufPh0wn4KFtpGswaTELMAkGA1UEBhMCVVMx
FTATBgNVBAgTDFBlbm5zeWx2YW5pYTEVMBMGA1UEBxMMUGhpbGFkZWxwaGlhMQww
CgYDVQQKEwNEb0QxDDAKBgNVBAsTA0REUzEQMA4GA1UEAxMHQVRBVCBDQYIJAK4J
Go3BBGhVMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABguwdFk42YP
8U6Du5HQ6Is1jfc1KEOowdh0d2MCH8q0KNktqiu6kWzjH1gRjRwc07bAkAWqXPB6
6gkRGYe/FRgi2Rn+Uo5UC5ahI4cXkE8OitCIEP3Br9fUw+vj/3Iiov0QZ6Hv81Kl
ZTZhLiZbjAg5maL/vufnUp+n15qzm67APh3/2hcgO93UlE9o9vXohWy1lHs8u12o
hPLxghSmGc9eKalEWEs61OrohpOtCHUEd1isq76WhaiXSwSUrBxgy89Z517A7ffC
BjzLo5AVo6a9ou+ONVeZk8qw6YR6X9J7axy8YuTWt+Z82WFvOF0ubkqjm72d001M
7R9zCOQ3O+g=
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAzYU7UbstArnnVliaC/TB6VirkVWMnAEYMUZA1BKP8DZaNEKb
zFH2+mMw7O0BY7Ph9x0hEZ1kXLr6U93xcKyUWNPo13i5EwUUCSh2MdPfS8ZZt8DU
IIKC7XzFnKyKSKQmr0Mt9dC44rryPKTBvmI60rQ8VZkFEgvs8FCP0M4Ar6/gtJ24
ZLEtilu5dQBSlru4nPGXg07r2C2JgEZWshtMBtbHLkOM2gtp/pkYCCG0zqeU+0s3
H8IqDq0uYkONOfVeCumbg1/AtjgrZu7aOVPKyibkaI6sTTooXE5aSZkfkx0z6+fK
M2nPSe30HgiBODtb7G+44ln08d0isjpQ67OvGQIDAQABAoIBAHR4EInc3UEyQVu5
knM8Hbgzu+b86FZweFlUSuDkNBYZdz0ukkRUHvb+x3c9SRBLnL8CDv+AhqPWgo6M
tIr6Aofkb4vMqnWQ5y3ZdEIApAa5PZbY/F4AGFql3wdO8H8CJ7ojBCTOSDiVYTnk
1Lcjy9okshyAP1Ne1sPJo/bdB56HtXs+wqok1NntIQwiXjjD9xUuc1EZk0J4M97L
vBUjUGNX942UjtRiey5zwhRp3bTPasTduHcA01NaIbOVYlRFwc2W+cflz0l6ml2p
14TNEEvIMMMCNKnlPrpGI23n0psAvE4nbuxZQGVYAFvXrWn+Gyvz0Yag2EoMUCEs
ziLED9ECgYEA6IByu+xqIuIAhj/PwIIxV4+lkuV4TXIlfAFLR4JuokOVfbRsmu2e
9EfeOUD9LfQ4KsG5mu4Abpja0k/VKRKRGRjV6Oe2C6VK942HFP6Kpn0hgIuomZkD
eVv8naDezZjAvVace38zjRWB2GXTpapwBAgf/YflPPsDZ8bi/weqZCMCgYEA4kqx
Ka489Rr7+cSXpMeS5lLufhlaE5OVQc5HVFREDAI5vXU8BM2sLiHTC/BHjis2JvLm
aRJ0UsxUoIUURl2KjTbx3zns4HDVkzBrSpoDXWxBjAo0oEg7JVc+6+qEqbDHHS1L
/UJ6mlUegsE42MkFWG3YJQuHxyLZqPXIwNAyhZMCgYEA5cxnGnSt5rJoAEi7xzMn
H7s71Hf3stw6TlldFV3GiZyw+aDFo09vR1RtQTuJwczbYu88yvOn+6gax7neHo1a
WmrgqiWzGcmS0iDRPZ/kXG/bGBlxV/cTpvSTNx0UejMbdUhQvANaaXyzbLYgPWK6
+lEphUW2/tG+aOj73UOvVu8CgYA5L8sJz4CUKJeZDTeNauoSzs56i4mZ/OfxU2Hv
S8ROjJlu6ZubUya6Gc4t7DEJGp56xVO5JfLDoeOZFUiEZ8tF2KbTVN4p8hnnMotK
tRU4nM0LyOB3yQk5bIz4LbIM+CG5m+LiQ9Sb//rP7GijUFnLeSbwZbOQfZwn+MUd
BQBfhQKBgQDmuX8tJdPkjE133IhQhZHbHHt6AEQA3aXkFdvPvbYD9VbGTZ8wnpFO
VJrDDWnIKAgO2FerIX9oq+H9a5fggYtTMeAX1cOA6b9SnLmFjt0utxrQKxf7p5I+
n+EsmcAWfb+KRQwoB0L/mE9Ool14AeJ15kHyNIrCrMPv0J4zoC0Jdg==
-----END RSA PRIVATE KEY-----

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