commit
667a2cb5b2
7
.gitignore
vendored
7
.gitignore
vendored
@ -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
|
||||
|
20
.travis.yml
20
.travis.yml
@ -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
21
Pipfile
@ -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
464
Pipfile.lock
generated
@ -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"
|
||||
|
37
README.md
37
README.md
@ -1,22 +1,22 @@
|
||||
|
||||
# ATST
|
||||
|
||||
[](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
74
alembic.ini
Normal 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
1
alembic/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
84
alembic/env.py
Normal file
84
alembic/env.py
Normal 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
24
alembic/script.py.mako
Normal 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"}
|
38
alembic/versions/1f57f784ed5b_update_user_from_authnid.py
Normal file
38
alembic/versions/1f57f784ed5b_update_user_from_authnid.py
Normal 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 ###
|
39
alembic/versions/4ea5917e7781_add_default_atat_role.py
Normal file
39
alembic/versions/4ea5917e7781_add_default_atat_role.py
Normal 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'")
|
||||
|
62
alembic/versions/4ede1e3e50d1_add_authz_models.py
Normal file
62
alembic/versions/4ede1e3e50d1_add_authz_models.py
Normal 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 ###
|
38
alembic/versions/55ba973d08b9_add_fundz_models.py
Normal file
38
alembic/versions/55ba973d08b9_add_fundz_models.py
Normal 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 ###
|
183
alembic/versions/96a9f3537996_add_roles_and_permissions.py
Normal file
183
alembic/versions/96a9f3537996_add_roles_and_permissions.py
Normal 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'
|
||||
);
|
||||
""")
|
47
alembic/versions/b5b17d465166_requests.py
Normal file
47
alembic/versions/b5b17d465166_requests.py
Normal 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 ###
|
26
alembic/versions/ff1c9c02fa61_enable_uuid_extension.py
Normal file
26
alembic/versions/ff1c9c02fa61_enable_uuid_extension.py
Normal 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
12
app.py
@ -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()
|
||||
|
@ -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
|
294
atst/app.py
294
atst/app.py
@ -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)
|
||||
|
||||
|
@ -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
3
atst/database.py
Normal file
@ -0,0 +1,3 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
32
atst/domain/auth.py
Normal file
32
atst/domain/auth.py
Normal 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
|
||||
|
0
atst/domain/authnid/__init__.py
Normal file
0
atst/domain/authnid/__init__.py
Normal file
72
atst/domain/authnid/crl/util.py
Normal file
72
atst/domain/authnid/crl/util.py
Normal 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")
|
124
atst/domain/authnid/crl/validator.py
Normal file
124
atst/domain/authnid/crl/validator.py
Normal 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
|
13
atst/domain/authnid/utils.py
Normal file
13
atst/domain/authnid/utils.py
Normal 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
16
atst/domain/exceptions.py
Normal 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
25
atst/domain/pe_numbers.py
Normal 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
134
atst/domain/requests.py
Normal 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
21
atst/domain/roles.py
Normal 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()
|
19
atst/domain/task_orders.py
Normal file
19
atst/domain/task_orders.py
Normal 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
65
atst/domain/users.py
Normal 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
|
73
atst/domain/workspace_users.py
Normal file
73
atst/domain/workspace_users.py
Normal 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
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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=[
|
||||
|
@ -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()]
|
||||
)
|
||||
|
@ -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)?"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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"]
|
@ -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
|
@ -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
|
@ -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)
|
@ -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"]
|
@ -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
|
||||
)
|
@ -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
|
@ -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"
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -1,3 +0,0 @@
|
||||
from unipath import Path
|
||||
|
||||
home = Path(__file__).parent.parent
|
12
atst/models/__init__.py
Normal file
12
atst/models/__init__.py
Normal 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
15
atst/models/pe_number.py
Normal 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
|
||||
)
|
40
atst/models/permissions.py
Normal file
40
atst/models/permissions.py
Normal 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
32
atst/models/request.py
Normal 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)
|
21
atst/models/request_status_event.py
Normal file
21
atst/models/request_status_event.py
Normal 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
14
atst/models/role.py
Normal 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
10
atst/models/task_order.py
Normal 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
11
atst/models/types.py
Normal 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
30
atst/models/user.py
Normal 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)
|
24
atst/models/workspace_role.py
Normal file
24
atst/models/workspace_role.py
Normal 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,
|
||||
)
|
14
atst/models/workspace_user.py
Normal file
14
atst/models/workspace_user.py
Normal 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
61
atst/routes/__init__.py
Normal 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
58
atst/routes/dev.py
Normal 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)
|
7
atst/routes/requests/__init__.py
Normal file
7
atst/routes/requests/__init__.py
Normal 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
|
44
atst/routes/requests/financial_verification.py
Normal file
44
atst/routes/requests/financial_verification.py
Normal 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
|
35
atst/routes/requests/index.py
Normal file
35
atst/routes/requests/index.py
Normal 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)
|
128
atst/routes/requests/jedi_request_flow.py
Normal file
128
atst/routes/requests/jedi_request_flow.py
Normal 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
|
98
atst/routes/requests/requests_form.py
Normal file
98
atst/routes/requests/requests_form.py
Normal 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
38
atst/routes/workspaces.py
Normal 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
|
||||
)
|
@ -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
|
@ -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)
|
@ -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)
|
@ -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
|
||||
|
@ -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
2
config/prod.ini
Normal file
@ -0,0 +1,2 @@
|
||||
[default]
|
||||
SESSION_COOKIE_SECURE=True
|
4
config/test.ini
Normal file
4
config/test.ini
Normal file
@ -0,0 +1,4 @@
|
||||
[default]
|
||||
PGDATABASE = atat_test
|
||||
CRL_DIRECTORY = tests/fixtures/crl
|
||||
WTF_CSRF_ENABLED = false
|
@ -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
9
ipython_setup.py
Normal 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
4
js/example.js
Normal file
@ -0,0 +1,4 @@
|
||||
window.onload = function() {
|
||||
console.log('hello from example')
|
||||
}
|
||||
|
5
js/index.js
Normal file
5
js/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import classes from '../styles/atat.scss'
|
||||
|
||||
import './example'
|
||||
|
||||
console.log('hello from index')
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
9
script/console
Executable 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
8
script/get_db_settings
Executable 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
|
33
script/ingest_pe_numbers.py
Normal file
33
script/ingest_pe_numbers.py
Normal 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)
|
@ -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}"
|
||||
|
@ -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
16
script/sync-crls
Executable 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
25
script/sync-dod-certs
Executable 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
|
@ -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"
|
||||
|
||||
|
@ -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"
|
||||
|
25
ssl/certificate-authority/ca.crt
Normal file
25
ssl/certificate-authority/ca.crt
Normal 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-----
|
27
ssl/certificate-authority/ca.key
Normal file
27
ssl/certificate-authority/ca.key
Normal 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
Loading…
x
Reference in New Issue
Block a user