Merge pull request #1291 from dod-ccpo/staging

Update Master
This commit is contained in:
dandds 2020-01-13 11:03:47 -05:00 committed by GitHub
commit 410273fc5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
231 changed files with 16444 additions and 13260 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
terraform/* @dandds

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null
},
"generated_at": "2019-12-06T21:22:07Z",
"generated_at": "2020-01-09T11:35:03Z",
"plugins_used": [
{
"base64_limit": 4.5,
@ -98,7 +98,7 @@
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_secret": false,
"is_verified": false,
"line_number": 29,
"line_number": 30,
"type": "Secret Keyword"
}
],
@ -111,15 +111,6 @@
"type": "Secret Keyword"
}
],
"ssl/certificate-authority/ca.key": [
{
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
"is_secret": false,
"is_verified": false,
"line_number": 1,
"type": "Private Key"
}
],
"ssl/client-certs/atat.mil.key": [
{
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
@ -170,7 +161,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false,
"is_verified": false,
"line_number": 656,
"line_number": 665,
"type": "Hex High Entropy String"
}
]

View File

@ -84,8 +84,7 @@ COPY --from=builder /install/celery_worker.py ./celery_worker.py
COPY --from=builder /install/config/ ./config/
COPY --from=builder /install/templates/ ./templates/
COPY --from=builder /install/translations.yaml .
COPY --from=builder /install/script/seed_roles.py ./script/seed_roles.py
COPY --from=builder /install/script/sync-crls ./script/sync-crls
COPY --from=builder /install/script/ ./script/
COPY --from=builder /install/static/ ./static/
COPY --from=builder /install/fixtures/ ./fixtures
COPY --from=builder /install/uwsgi.ini .

View File

@ -18,7 +18,6 @@ flask-session = "*"
flask-wtf = "*"
pyopenssl = "*"
requests = "*"
apache-libcloud = "*"
lockfile = "*"
werkzeug = "*"
PyYAML = "*"
@ -30,6 +29,7 @@ azure-graphrbac = "*"
msrestazure = "*"
azure-mgmt-authorization = "*"
azure-mgmt-managementgroups = "*"
azure-mgmt-resource = "*"
[dev-packages]
bandit = "*"

225
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "c2b19c436646705ea3bf4df8c35c2833083f048da37fc619e66f7236153607c5"
"sha256": "c203c47b00f413fd40056ef6d2d8e51b37ad3ff5f7693db5eb170b7f8fd43234"
},
"pipfile-spec": 6,
"requires": {
@ -25,10 +25,10 @@
},
"alembic": {
"hashes": [
"sha256:49277bb7242192bbb9eac58fed4fe02ec6c3a2a4b4345d2171197459266482b2"
"sha256:3b0cb1948833e062f4048992fbc97ecfaaaac24aaa0d83a1202a99fb58af8c6d"
],
"index": "pypi",
"version": "==1.3.1"
"version": "==1.3.2"
},
"amqp": {
"hashes": [
@ -37,14 +37,6 @@
],
"version": "==2.5.2"
},
"apache-libcloud": {
"hashes": [
"sha256:9bc5cd5c32151bb7a04a7c7de0be9b4a4b8271e348ac91dd79eaaeeae627115f",
"sha256:fcc165f2cc2db9a379c6d3a17b3beb9081bb64ba5c0bf7bbb58da864810092f0"
],
"index": "pypi",
"version": "==2.6.1"
},
"azure-common": {
"hashes": [
"sha256:53b1195b8f20943ccc0e71a17849258f7781bc6db1c72edc7d6c055f79bd54e3",
@ -76,6 +68,14 @@
"index": "pypi",
"version": "==0.2.0"
},
"azure-mgmt-resource": {
"hashes": [
"sha256:20b3394e4dc76fbd9459723cb8c0300fb18a8c32100076f023b5470426b9f104",
"sha256:eaea8b5d05495d1b74220052275d46b6bed93b59245bcaa747279a52e41c3bdf"
],
"index": "pypi",
"version": "==7.0.0"
},
"azure-mgmt-subscription": {
"hashes": [
"sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1",
@ -117,11 +117,11 @@
},
"celery": {
"hashes": [
"sha256:65f4d67fc1037edacecbf39fcf956da68b984cf2a6d89bd73a8a5a80e35e3dd7",
"sha256:8a59d80235b876881d9893751f2a87936165fc1347efee66095620b3cadf533b"
"sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f",
"sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"
],
"index": "pypi",
"version": "==4.4.0rc4"
"version": "==4.4.0"
},
"certifi": {
"hashes": [
@ -256,10 +256,11 @@
},
"importlib-metadata": {
"hashes": [
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
],
"version": "==1.1.0"
"markers": "python_version < '3.8'",
"version": "==1.3.0"
},
"isodate": {
"hashes": [
@ -284,10 +285,10 @@
},
"kombu": {
"hashes": [
"sha256:1760b54b1d15a547c9a26d3598a1c8cdaf2436386ac1f5561934bc8a3cbbbd86",
"sha256:e7465aa85a1db889116819f08c5de29520d2fa103324dcdca5e90af345f01771"
"sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac",
"sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"
],
"version": "==4.6.6"
"version": "==4.6.7"
},
"lockfile": {
"hashes": [
@ -338,10 +339,10 @@
},
"more-itertools": {
"hashes": [
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
],
"version": "==8.0.0"
"version": "==8.0.2"
},
"msrest": {
"hashes": [
@ -514,10 +515,10 @@
},
"sqlalchemy": {
"hashes": [
"sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a"
"sha256:bfb8f464a5000b567ac1d350b9090cf081180ec1ab4aa87e7bca12dab25320ec"
],
"index": "pypi",
"version": "==1.3.11"
"version": "==1.3.12"
},
"unipath": {
"hashes": [
@ -677,44 +678,46 @@
},
"colorama": {
"hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
"sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
"sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
],
"version": "==0.4.1"
"version": "==0.4.3"
},
"coverage": {
"hashes": [
"sha256:2358e685d0253125da42a48396038d4c7b4cd1448c00bbc4bda0cb8c43c2a870",
"sha256:25017cf384eeed2e6caf72efd3ec4124e32a8b7a4387600499104387975400c7",
"sha256:2e2de9423ff8b14303a97eafddd16c479fbcc9a0b8b0be3b7c3843a3e0cf6d69",
"sha256:324ed908e4e40a6e2451056fe502470ad4e79495cb7a03ecab94e6309c3e117e",
"sha256:34f865a0cf6255b694a46e4383a7131c61ea72c5b4c4f81d20e522fb1e440b4b",
"sha256:3a2bcc464b60a18f1f7167b95b2773ede93bf3722bfa59e0802717f652b6cc25",
"sha256:48d70865266d649b6602e2ba94820d7972ef470d3b72a8fd41a3d17321feed3a",
"sha256:50cf23523ab3a724c6905d3b60f87fa8250d9bae3995e09f49f63effa2b54f15",
"sha256:54c84a68abd8c4c5b71878b35eb85321df41f3d144c78181867d5b026ec74994",
"sha256:5b59d661ee7f3200aedd7b71882b7927ea7ed522df75e3853f316a79ad872a2e",
"sha256:5ffb39624bc573177888a21fb301ccee46838c600b27d58c3e9dae495f44d34a",
"sha256:699b3072b7f0e69ed175a88fa8b2ec7eefc4f34d490c54ed9a52feff21a15fdc",
"sha256:79ef4a2bb862110bd585174e551a783bee5c3aa461734a2ac7429193be357589",
"sha256:8210a6f93c4a8c6d460b402e20e38399529b99200c3318542faf6a520c9b6a5c",
"sha256:8d30c10cfd0a6fdf0a2d5023de00ef7b329cd6ead2310c9e53eab79c209acb70",
"sha256:97ac79ff28f2cda6ac00a803ee582b965951755f61ab43377482bfba450b619a",
"sha256:9fe4aacacff9028ed167db108bf013510654f148d83c4857fed61d2ce0588bf2",
"sha256:a5b6395d5957d638f8b1870561607e3c39b1a236ea6cff9eafe5b9bb1db913f2",
"sha256:ab32c5fad6905986a7e34e3acf01180a69bb60c2aa7331815b46e51c776a1943",
"sha256:ad67f0cfdfecbd49b9da46a7e488e6dc32a69388740b85c36a4ef4b33082cbad",
"sha256:aedad67c30326a1af324f45833a40b97180664912deb29942459ddbe9fa0ce19",
"sha256:b077cd0e70f41366ac1f9d09275258fa1906758a5d4f31cacc18b10dfcf90784",
"sha256:b8ea210810d3c14aec7561f8fe0d3eec582d1088100aaa0bb8153d53d867d20f",
"sha256:bf572722326ce6704e863447a070039a827072b7179352570859be899b9e6551",
"sha256:c0df57e189dacd2606cae6386acf127d01d85b2bf49acd9a65543b5d6c359ddc",
"sha256:d523e75f2a8a0b4a6a8be1287c0e0e3a561b8832b05ddd987d4cd7c62f3ad3bc",
"sha256:e10593c60c5f0bfd8b241bf9f27ef2191a3005b73dde8ada0424f642543a1e59",
"sha256:e9128444c83bc260aea988bf1ca6278a33ba730955bf94720468c656b61353eb",
"sha256:f7162f2e3711f3a08a8a741f92e1f63afd58d0713177979f2cf9723dd50161cf"
"sha256:0cd13a6e98c37b510a2d34c8281d5e1a226aaf9b65b7d770ef03c63169965351",
"sha256:1a4b6b6a2a3a6612e6361130c2cc3dc4378d8c221752b96167ccbad94b47f3cd",
"sha256:2ee55e6dba516ddf6f484aa83ccabbb0adf45a18892204c23486938d12258cde",
"sha256:3be5338a2eb4ef03c57f20917e1d12a1fd10e3853fed060b6d6b677cb3745898",
"sha256:44b783b02db03c4777d8cf71bae19eadc171a6f2a96777d916b2c30a1eb3d070",
"sha256:475bf7c4252af0a56e1abba9606f1e54127cdf122063095c75ab04f6f99cf45e",
"sha256:47c81ee687eafc2f1db7f03fbe99aab81330565ebc62fb3b61edfc2216a550c8",
"sha256:4a7f8e72b18f2aca288ff02255ce32cc830bc04d993efbc87abf6beddc9e56c0",
"sha256:50197163a22fd17f79086e087a787883b3ec9280a509807daf158dfc2a7ded02",
"sha256:56b13000acf891f700f5067512b804d1ec8c301d627486c678b903859d07f798",
"sha256:79388ae29c896299b3567965dbcd93255f175c17c6c7bca38614d12718c47466",
"sha256:79fd5d3d62238c4f583b75d48d53cdae759fe04d4fb18fe8b371d88ad2b6f8be",
"sha256:7fe3e2fde2bf1d7ce25ebcd2d3de3650b8d60d9a73ce6dcef36e20191291613d",
"sha256:81042a24f67b96e4287774014fa27220d8a4d91af1043389e4d73892efc89ac6",
"sha256:81326f1095c53111f8afc95da281e1414185f4a538609a77ca50bdfa39a6c207",
"sha256:8873dc0d8f42142ea9f20c27bbdc485190fff93823c6795be661703369e5877d",
"sha256:88d2cbcb0a112f47eef71eb95460b6995da18e6f8ca50c264585abc2c473154b",
"sha256:91f2491aeab9599956c45a77c5666d323efdec790bfe23fcceafcd91105d585a",
"sha256:979daa8655ae5a51e8e7a24e7d34e250ae8309fd9719490df92cbb2fe2b0422b",
"sha256:9c871b006c878a890c6e44a5b2f3c6291335324b298c904dc0402ee92ee1f0be",
"sha256:a6d092545e5af53e960465f652e00efbf5357adad177b2630d63978d85e46a72",
"sha256:b5ed7837b923d1d71c4f587ae1539ccd96bfd6be9788f507dbe94dab5febbb5d",
"sha256:ba259f68250f16d2444cbbfaddaa0bb20e1560a4fdaad50bece25c199e6af864",
"sha256:be1d89614c6b6c36d7578496dc8625123bda2ff44f224cf8b1c45b810ee7383f",
"sha256:c1b030a79749aa8d1f1486885040114ee56933b15ccfc90049ba266e4aa2139f",
"sha256:c95bb147fab76f2ecde332d972d8f4138b8f2daee6c466af4ff3b4f29bd4c19e",
"sha256:d52c1c2d7e856cecc05aa0526453cb14574f821b7f413cc279b9514750d795c1",
"sha256:d609a6d564ad3d327e9509846c2c47f170456344521462b469e5cb39e48ba31c",
"sha256:e1bad043c12fb58e8c7d92b3d7f2f49977dcb80a08a6d1e7a5114a11bf819fca",
"sha256:e5a675f6829c53c87d79117a8eb656cc4a5f8918185a32fc93ba09778e90f6db",
"sha256:fec32646b98baf4a22fdceb08703965bd16dea09051fbeb31a04b5b6e72b846c"
],
"version": "==5.0b1"
"version": "==5.0"
},
"decorator": {
"hashes": [
@ -747,10 +750,10 @@
},
"faker": {
"hashes": [
"sha256:48c03580720e0b46538d528b1296e4e5b24a809dcaf33a7dddec719489a9edb8",
"sha256:6327c665c0d8721280b3036d9c9e851c60092bc1f30c8394cc433f8723e2bda5"
"sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11",
"sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432"
],
"version": "==2.0.4"
"version": "==3.0.0"
},
"flask": {
"hashes": [
@ -791,25 +794,26 @@
},
"importlib-metadata": {
"hashes": [
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
],
"version": "==1.1.0"
"markers": "python_version < '3.8'",
"version": "==1.3.0"
},
"ipdb": {
"hashes": [
"sha256:473fdd798a099765f093231a8b1fabfa95b0b682fce12de0c74b61a4b4d8ee57"
"sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd"
],
"index": "pypi",
"version": "==0.12.2"
"version": "==0.12.3"
},
"ipython": {
"hashes": [
"sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51",
"sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e"
"sha256:190a279bd3d4fc585a611e9358a88f1048cc57fd688254a86f9461889ee152a6",
"sha256:762d79a62b6aa96b04971e920543f558dfbeedc0468b899303c080c8068d4ac2"
],
"index": "pypi",
"version": "==7.10.1"
"version": "==7.10.2"
},
"ipython-genutils": {
"hashes": [
@ -914,30 +918,29 @@
},
"more-itertools": {
"hashes": [
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
],
"version": "==8.0.0"
"version": "==8.0.2"
},
"mypy": {
"hashes": [
"sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768",
"sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646",
"sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c",
"sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939",
"sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279",
"sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2",
"sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2",
"sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640",
"sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7",
"sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c",
"sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17",
"sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78",
"sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931",
"sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a"
"sha256:0308c35fd16c96a81b8dfc4d09ec63b8fa607cfec087acf5aafb44c2c45197de",
"sha256:39f7be2f89668d21b2bbab45ce5aa15e69bf8d6f3b46f9e1cc1a88e4fcc84f3d",
"sha256:4223f576813c79a10d0fd14192c86f1b85e3bd235c93792f22ed811a20b5ee4e",
"sha256:4c8f812a2fbefa96185933fbe05aa035e9cf791cf3a23bbdb6a219c80b60e0b1",
"sha256:4ea9ee847ea5bb38ea275441f3aea7eeba1b96187a3f968ee359d33d9dcc0eda",
"sha256:573c68df69f0e399fa57866a0b72989acf0a56c4008eee59c789c2ca5ea9df03",
"sha256:588c0e38466306aa7dbe6522ceacf37dde8b13cfa5edde90be2ce382f078875f",
"sha256:6d1bd2e675823a19e6bf72149540ab9851bfe698b796aea698fb926ab2bedd02",
"sha256:aa8e3bd1540dd5c39ef580ec2146a9c99c45f7c62af890095fec9e87b5ca19fb",
"sha256:b978ba1ea90d0abe2fc720ec9a41824b7d3a1304569bd58c9038d8d61dc4dfdb",
"sha256:c85c5367c2e8247e06cc0aba84e3633e90f48e8a0677bc51b351e138b5ff80b1",
"sha256:ce69577b424058bfa177df27213869f37c1e964c3e1ebd3b3d54f1d10b234c4d",
"sha256:ec6eaf98a57624d96d9916352a5bad2d73959f6358fabf43838f7d1a4d2f8389"
],
"index": "pypi",
"version": "==0.750"
"version": "==0.760"
},
"mypy-extensions": {
"hashes": [
@ -948,10 +951,10 @@
},
"parso": {
"hashes": [
"sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc",
"sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"
"sha256:55cf25df1a35fd88b878715874d2c4dc1ad3f0eebd1e0266a67e1f55efccfbe1",
"sha256:5c1f7791de6bd5dbbeac8db0ef5594b36799de198b3f7f7014643b0c5536b9d3"
],
"version": "==0.5.1"
"version": "==0.5.2"
},
"pathspec": {
"hashes": [
@ -1063,11 +1066,11 @@
},
"pytest-mock": {
"hashes": [
"sha256:96a0cebc66e09930be2a15b03333d90b59584d3fb011924f81c14b50ee0afbba",
"sha256:e5381be2608e49547f5e47633c5f81241ebf6206d17ce516a7a18d5a917e3859"
"sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d",
"sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"
],
"index": "pypi",
"version": "==1.12.1"
"version": "==1.13.0"
},
"pytest-watch": {
"hashes": [
@ -1102,21 +1105,29 @@
},
"regex": {
"hashes": [
"sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7",
"sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7",
"sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96",
"sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1",
"sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69",
"sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910",
"sha256:7faf534c1841c09d8fefa60ccde7b9903c9b528853ecf41628689793290ca143",
"sha256:b4e0406d822aa4993ac45072a584d57aa4931cf8288b5455bbf30c1d59dbad59",
"sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2",
"sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66",
"sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6",
"sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a",
"sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74"
"sha256:0472acc4b6319801c1bc681d838c88ba1446f9ae199e01f6e41091c701fb3d42",
"sha256:16709434c4e2332ee8ba26ae339aceb8ab0b24b8398ebd0f52ebc943f45c4fc2",
"sha256:223fb63ec8dcab20b3318e93dcec4aee89e98b062934090bf29ffc374d2000a2",
"sha256:23c3ebf05d1cd3adb26723fd598e75724e0cdb7d6a35185ac0caf061cc6edb49",
"sha256:2404a50fb48badaf214b700f08822b68d93d79200e0aefd9569d0332d21fbfcb",
"sha256:2af3a7a16fed6eff85c25da106effa36f61cbbe801d00ade349b53ce7619eb15",
"sha256:37e018d3746baf159aedfc9773c3cafacbd10d354ba15484f5cfc8ed9da5748b",
"sha256:3c9c2988d02a9238a1975c70e87c6ce94e6f36dd8e372b66f468990cfe077434",
"sha256:47298bc8b89d1c747f0f5974aa528fc0b6b17396f1694136a224d51461279d83",
"sha256:4eeb0fe936797ae00a085f99802642bfc722b3b4ea557e9e7849cb621ea10c91",
"sha256:6881be0218b47ed76db033f252bab3f912dfe7ed1fe7baa9daebf51de08546a0",
"sha256:7ac08cee5055f548eed3889e9aaef15fd00172d037949496f1f0b34acb8a7c3e",
"sha256:7c5e2efcf079c35ff266c3f3a6708834f88f9fd04a3c16b855e036b2b7b1b543",
"sha256:8355eaa64724a0fdb010a1654b77cb3e375dc08b7f592cc4a1c05ac606aa481c",
"sha256:999a885f7f5194464238ad5d74b05982acee54002f3aa775d8e0e8c5fb74c06c",
"sha256:9fd2f4813eaa3e421e82819d38e5b634d900faff7ae5a80cd89ccff407175e69",
"sha256:a2e1e53df7dd27943da2b512895125b33fb20f81862c9fed7b3bab2a1de684d1",
"sha256:ab43bc0836820b7900dfffc025b996784aec26ec87dc1df4f95a40398760223f",
"sha256:ba449b56fa419fb19bf2a2438adbd2433f27087a6fe115917eaf9cfca684d5b6",
"sha256:d3f632cefad2cf247bd845794002585e3772288bfcb0dbac59fdecd32cd38b67",
"sha256:d51311496061863caae2cfe120cf1ef37900019b86c89c2d75f0918e0b4b8bf3"
],
"version": "==2019.11.1"
"version": "==2019.12.19"
},
"requests": {
"hashes": [

View File

@ -254,6 +254,7 @@ To generate coverage reports for the Javascript tests:
- `SECRET_KEY`: String key which will be used to sign the session cookie. Should be a long string of random bytes. https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY
- `SERVER_NAME`: Hostname for ATAT. Only needs to be specified in contexts where the hostname cannot be inferred from the request, such as Celery workers. https://flask.palletsprojects.com/en/1.1.x/config/#SERVER_NAME
- `SESSION_COOKIE_NAME`: String value specifying the name to use for the session cookie. https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_NAME
- `SESSION_COOKIE_DOMAIN`: String value specifying the name to use for the session cookie. This should be set to the root domain so that it is valid for both the main site and the authentication subdomain. https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_DOMAIN
- `SESSION_TYPE`: String value specifying the cookie storage backend. https://pythonhosted.org/Flask-Session/
- `SESSION_USE_SIGNER`: Boolean value specifying if the cookie sid should be signed.
- `SQLALCHEMY_ECHO`: Boolean value specifying if SQLAlchemy should log queries to stdout.

View File

@ -0,0 +1,36 @@
"""update portfolios defense component column type
Revision ID: 02ac8bdcf16f
Revises: 08f2a640e9c2
Create Date: 2019-12-26 16:10:54.366461
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '02ac8bdcf16f' # pragma: allowlist secret
down_revision = '08f2a640e9c2' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.alter_column('portfolios', 'defense_component',
type_=postgresql.ARRAY(sa.VARCHAR()),
existing_type=sa.VARCHAR(),
postgresql_using="string_to_array(defense_component, ',')::character varying[]",
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('portfolios', 'defense_component',
type_=sa.VARCHAR(),
existing_type=postgresql.ARRAY(sa.VARCHAR()),
postgresql_using="defense_component[1]::character varying",
nullable=False)
# ### end Alembic commands ###

View File

@ -0,0 +1,26 @@
"""add uniqueness contraint to environment within an application
Revision ID: 08f2a640e9c2
Revises: c487d91f1a26
Create Date: 2019-12-16 10:43:12.331095
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = '08f2a640e9c2' # pragma: allowlist secret
down_revision = 'c487d91f1a26' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint('environments_name_application_id_key', 'environments', ['name', 'application_id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('environments_name_application_id_key', 'environments', type_='unique')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""remove users.provisional
Revision ID: 5d7198d34b91
Revises: 02ac8bdcf16f
Create Date: 2020-01-09 08:42:34.512191
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5d7198d34b91' # pragma: allowlist secret
down_revision = '02ac8bdcf16f' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'provisional')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('provisional', sa.BOOLEAN(), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@ -0,0 +1,27 @@
"""add application name and portfolio_id unique constraint
Revision ID: c487d91f1a26
Revises: 3bd8552f1c57
Create Date: 2019-12-13 14:33:23.952450
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = 'c487d91f1a26' # pragma: allowlist secret
down_revision = '3bd8552f1c57' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint('applications_name_portfolio_id_key', 'applications', ['name', 'portfolio_id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('applications_name_portfolio_id_key', 'applications', type_='unique')
# ### end Alembic commands ###

View File

@ -11,7 +11,7 @@ from atst.models import (
ApplicationRoleStatus,
EnvironmentRole,
)
from atst.utils import first_or_none
from atst.utils import first_or_none, commit_or_raise_already_exists_error
class Applications(BaseDomainClass):
@ -28,7 +28,7 @@ class Applications(BaseDomainClass):
if environment_names:
Environments.create_many(user, application, environment_names)
db.session.commit()
commit_or_raise_already_exists_error(message="application")
return application
@classmethod
@ -53,9 +53,9 @@ class Applications(BaseDomainClass):
Environments.create_many(
g.current_user, application, new_data["environment_names"]
)
db.session.add(application)
db.session.commit()
db.session.add(application)
commit_or_raise_already_exists_error(message="application")
return application
@classmethod

View File

@ -1,4 +1,4 @@
from flask import g, redirect, url_for, session, request
from flask import g, redirect, url_for, session, request, current_app as app
from atst.domain.users import Users
@ -59,8 +59,10 @@ def get_last_login():
def logout():
if session.get("user_id"): # pragma: no branch
dod_id = g.current_user.dod_id
del session["user_id"]
del session["last_login"]
app.logger.info(f"user with EDIPI {dod_id} has logged out")
def _unprotected_route(request):

View File

@ -276,7 +276,7 @@ def existing_crl_modification_time(crl):
prev_time = os.path.getmtime(crl)
buffered = prev_time + MODIFIED_TIME_BUFFER
mod_time = prev_time if pendulum.now().timestamp() < buffered else buffered
dt = pendulum.from_timestamp(mod_time, tz="GMT")
dt = pendulum.from_timestamp(mod_time, tz="UTC")
return dt.format("ddd, DD MMM YYYY HH:mm:ss zz")
else:

View File

@ -6,6 +6,7 @@ from atst.models.user import User
from atst.models.application import Application
from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole
from .policy import AzurePolicyManager
class GeneralCSPException(Exception):
@ -401,6 +402,7 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
class AzureSDKProvider(object):
def __init__(self):
from azure.mgmt import subscription, authorization, managementgroups
from azure.mgmt.resource import policy
import azure.graphrbac as graphrbac
import azure.common.credentials as credentials
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
@ -410,6 +412,7 @@ class AzureSDKProvider(object):
self.managementgroups = managementgroups
self.graphrbac = graphrbac
self.credentials = credentials
self.policy = policy
# may change to a JEDI cloud
self.cloud = AZURE_PUBLIC_CLOUD
@ -427,6 +430,8 @@ class AzureCloudProvider(CloudProviderInterface):
else:
self.sdk = azure_sdk_provider
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
def create_environment(
self, auth_credentials: Dict, user: User, environment: Environment
):
@ -561,6 +566,51 @@ class AzureCloudProvider(CloudProviderInterface):
# but we just don't have a valid ID
pass
AZURE_MANAGEMENT_API = "https://management.azure.com"
def _create_policy_definition(
self, credentials, subscription_id, management_group_id, properties,
):
"""
Requires credentials that have AZURE_MANAGEMENT_API
specified as the resource. The Service Principal
specified in the credentials must have the "Resource
Policy Contributor" role assigned with a scope at least
as high as the management group specified by
management_group_id.
Arguments:
credentials -- ServicePrincipalCredentials
subscription_id -- str, ID of the subscription (just the UUID, not the path)
management_group_id -- str, ID of the management group (just the UUID, not the path)
properties -- dictionary, the "properties" section of a valid Azure policy definition document
Returns:
azure.mgmt.resource.policy.[api version].models.PolicyDefinition: the PolicyDefinition object provided to Azure
Raises:
TBD
"""
# TODO: which subscription would this be?
client = self.sdk.policy.PolicyClient(credentials, subscription_id)
definition = client.policy_definitions.models.PolicyDefinition(
policy_type=properties.get("policyType"),
mode=properties.get("mode"),
display_name=properties.get("displayName"),
description=properties.get("description"),
policy_rule=properties.get("policyRule"),
parameters=properties.get("parameters"),
)
name = properties.get("displayName")
return client.policy_definitions.create_or_update_at_management_group(
policy_definition_name=name,
parameters=definition,
management_group_id=management_group_id,
)
def _get_management_service_principal(self):
# we really should be using graph.microsoft.com, but i'm getting
# "expired token" errors for that

47
atst/domain/csp/policy.py Normal file
View File

@ -0,0 +1,47 @@
from glob import glob
import json
from dataclasses import dataclass
from os.path import join as path_join
class AzurePolicyManager:
def __init__(self, static_policy_location):
self._static_policy_location = static_policy_location
@property
def portfolio_definitions(self):
if getattr(self, "_portfolio_definitions", None) is None:
portfolio_files = self._glob_json("portfolios")
self._portfolio_definitions = self._load_policies(portfolio_files)
return self._portfolio_definitions
@property
def application_definitions(self):
pass
@property
def environment_definitions(self):
pass
def _glob_json(self, path):
return glob(path_join(self._static_policy_location, "portfolios", "*.json"))
def _load_policies(self, json_policies):
return [self._load_policy(pol) for pol in json_policies]
def _load_policy(self, policy_file):
with open(policy_file, "r") as file_:
doc = json.loads(file_.read())
return AzurePolicy(
definition_point=doc["definitionPoint"],
definition=doc["policyDefinition"],
parameters=doc["parameters"],
)
@dataclass
class AzurePolicy:
definition_point: str
definition: dict
parameters: dict

View File

@ -34,16 +34,24 @@ class MockReportingProvider:
}
]
"""
if portfolio.name in cls.FIXTURE_SPEND_DATA:
applications = cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]
return sorted(
[
cls._get_application_monthly_totals(application)
for application in applications
],
key=lambda app: app["name"],
)
return []
fixture_apps = cls.FIXTURE_SPEND_DATA.get(portfolio.name, {}).get(
"applications", []
)
for application in portfolio.applications:
if application.name not in [app["name"] for app in fixture_apps]:
fixture_apps.append({"name": application.name, "environments": []})
return sorted(
[
cls._get_application_monthly_totals(portfolio, fixture_app)
for fixture_app in fixture_apps
if fixture_app["name"]
in [application.name for application in portfolio.applications]
],
key=lambda app: app["name"],
)
@classmethod
def _get_environment_monthly_totals(cls, environment):
@ -64,7 +72,7 @@ class MockReportingProvider:
}
@classmethod
def _get_application_monthly_totals(cls, application):
def _get_application_monthly_totals(cls, portfolio, fixture_app):
"""
returns a dictionary that represents spending totals for an application
and its environments e.g.
@ -83,19 +91,28 @@ class MockReportingProvider:
]
}
"""
environments = sorted(
[
cls._get_environment_monthly_totals(env)
for env in application["environments"]
],
key=lambda env: env["name"],
)
application_envs = [
env
for env in portfolio.all_environments
if env.application.name == fixture_app["name"]
]
environments = [
cls._get_environment_monthly_totals(env)
for env in fixture_app["environments"]
if env["name"] in [e.name for e in application_envs]
]
for env in application_envs:
if env.name not in [env["name"] for env in environments]:
environments.append({"name": env.name})
return {
"name": application["name"],
"this_month": sum(env["this_month"] for env in environments),
"last_month": sum(env["last_month"] for env in environments),
"total": sum(env["total"] for env in environments),
"environments": environments,
"name": fixture_app["name"],
"this_month": sum(env.get("this_month", 0) for env in environments),
"last_month": sum(env.get("last_month", 0) for env in environments),
"total": sum(env.get("total", 0) for env in environments),
"environments": sorted(environments, key=lambda env: env["name"]),
}
@classmethod

View File

@ -12,6 +12,7 @@ from atst.models import (
CLIN,
)
from atst.domain.environment_roles import EnvironmentRoles
from atst.utils import commit_or_raise_already_exists_error
from .exceptions import NotFoundError, DisabledError
@ -21,7 +22,7 @@ class Environments(object):
def create(cls, user, application, name):
environment = Environment(application=application, name=name, creator=user)
db.session.add(environment)
db.session.commit()
commit_or_raise_already_exists_error(message="environment")
return environment
@classmethod
@ -39,7 +40,8 @@ class Environments(object):
if name is not None:
environment.name = name
db.session.add(environment)
db.session.commit()
commit_or_raise_already_exists_error(message="environment")
return environment
@classmethod
def get(cls, environment_id):

View File

@ -75,10 +75,10 @@ class Portfolios(object):
permission_sets = PortfolioRoles._permission_sets_for_names(
member_data.get("permission_sets", [])
)
role = PortfolioRole(portfolio_id=portfolio.id, permission_sets=permission_sets)
role = PortfolioRole(portfolio=portfolio, permission_sets=permission_sets)
invitation = PortfolioInvitations.create(
inviter=inviter, role=role, member_data=member_data
inviter=inviter, role=role, member_data=member_data["user_data"]
)
PortfoliosQuery.add_and_commit(role)
@ -107,4 +107,7 @@ class Portfolios(object):
if "name" in new_data:
portfolio.name = new_data["name"]
if "description" in new_data:
portfolio.description = new_data["description"]
PortfoliosQuery.add_and_commit(portfolio)

View File

@ -1,11 +1,10 @@
import datetime
from sqlalchemy.exc import IntegrityError
from atst.database import db
from atst.models.clin import CLIN
from atst.models.task_order import TaskOrder, SORT_ORDERING
from . import BaseDomainClass
from .exceptions import AlreadyExistsError
from atst.utils import commit_or_raise_already_exists_error
class TaskOrders(BaseDomainClass):
@ -16,15 +15,8 @@ class TaskOrders(BaseDomainClass):
def create(cls, portfolio_id, number, clins, pdf):
task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
db.session.add(task_order)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
raise AlreadyExistsError("task_order")
commit_or_raise_already_exists_error(message="task_order")
TaskOrders.create_clins(task_order.id, clins)
return task_order
@classmethod
@ -42,12 +34,7 @@ class TaskOrders(BaseDomainClass):
task_order.number = number
db.session.add(task_order)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
raise AlreadyExistsError("task_order")
commit_or_raise_already_exists_error(message="task_order")
return task_order
@classmethod

View File

@ -117,12 +117,3 @@ class Users(object):
user.last_session_id = session_id
db.session.add(user)
db.session.commit()
@classmethod
def finalize(cls, user):
user.provisional = False
db.session.add(user)
db.session.commit()
return user

View File

@ -23,20 +23,10 @@ class PortfolioForm(BaseForm):
)
],
)
class PortfolioCreationForm(BaseForm):
name = StringField(
translate("forms.portfolio.name.label"),
validators=[
Length(
min=4,
max=100,
message=translate("forms.portfolio.name.length_validation_message"),
)
],
)
description = TextAreaField(translate("forms.portfolio.description.label"),)
class PortfolioCreationForm(PortfolioForm):
defense_component = SelectMultipleField(
choices=SERVICE_BRANCHES,
widget=ListWidget(prefix_label=False),

View File

@ -1,76 +1,59 @@
from wtforms.validators import Required
from wtforms.fields import StringField, FormField, FieldList, HiddenField
from wtforms.fields import BooleanField, FormField
from atst.domain.permission_sets import PermissionSets
from .forms import BaseForm
from .member import NewForm as BaseNewMemberForm
from atst.domain.permission_sets import PermissionSets
from atst.forms.fields import SelectField
from atst.utils.localization import translate
class PermissionsForm(BaseForm):
member_name = StringField()
member_id = HiddenField()
perms_app_mgmt = SelectField(
translate("forms.new_member.app_mgmt"),
choices=[
(
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
translate("common.view"),
),
(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
translate("common.edit"),
),
],
perms_app_mgmt = BooleanField(
translate("forms.new_member.app_mgmt.label"),
default=False,
description=translate("forms.new_member.app_mgmt.description"),
)
perms_funding = SelectField(
translate("forms.new_member.funding"),
choices=[
(PermissionSets.VIEW_PORTFOLIO_FUNDING, translate("common.view")),
(PermissionSets.EDIT_PORTFOLIO_FUNDING, translate("common.edit")),
],
perms_funding = BooleanField(
translate("forms.new_member.funding.label"),
default=False,
description=translate("forms.new_member.funding.description"),
)
perms_reporting = SelectField(
translate("forms.new_member.reporting"),
choices=[
(PermissionSets.VIEW_PORTFOLIO_REPORTS, translate("common.view")),
(PermissionSets.EDIT_PORTFOLIO_REPORTS, translate("common.edit")),
],
perms_reporting = BooleanField(
translate("forms.new_member.reporting.label"),
default=False,
description=translate("forms.new_member.reporting.description"),
)
perms_portfolio_mgmt = SelectField(
translate("forms.new_member.portfolio_mgmt"),
choices=[
(PermissionSets.VIEW_PORTFOLIO_ADMIN, translate("common.view")),
(PermissionSets.EDIT_PORTFOLIO_ADMIN, translate("common.edit")),
],
perms_portfolio_mgmt = BooleanField(
translate("forms.new_member.portfolio_mgmt.label"),
default=False,
description=translate("forms.new_member.portfolio_mgmt.description"),
)
@property
def data(self):
_data = super().data
_data["permission_sets"] = []
for field in _data:
if "perms" in field:
_data["permission_sets"].append(_data[field])
_data.pop("csrf_token", None)
perm_sets = []
if _data["perms_app_mgmt"]:
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
if _data["perms_funding"]:
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_FUNDING)
if _data["perms_reporting"]:
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_REPORTS)
if _data["perms_portfolio_mgmt"]:
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_ADMIN)
_data["permission_sets"] = perm_sets
return _data
class MembersPermissionsForm(BaseForm):
members_permissions = FieldList(FormField(PermissionsForm))
class NewForm(BaseForm):
class NewForm(PermissionsForm):
user_data = FormField(BaseNewMemberForm)
permission_sets = FormField(PermissionsForm)
@property
def update_data(self):
return {
"permission_sets": self.data.get("permission_sets").get("permission_sets"),
**self.data.get("user_data"),
}
class AssignPPOCForm(PermissionsForm):

View File

@ -151,3 +151,6 @@ class SignatureForm(BaseForm):
translate("task_orders.sign.digital_signature_description"),
validators=[Required()],
)
confirm = BooleanField(
translate("task_orders.sign.confirmation_description"), validators=[Required()],
)

View File

@ -1,4 +1,4 @@
from sqlalchemy import and_, Column, ForeignKey, String
from sqlalchemy import and_, Column, ForeignKey, String, UniqueConstraint
from sqlalchemy.orm import relationship, synonym
from atst.models.base import Base
@ -34,6 +34,11 @@ class Application(
),
)
members = synonym("roles")
__table_args__ = (
UniqueConstraint(
"name", "portfolio_id", name="applications_name_portfolio_id_key"
),
)
@property
def users(self):

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKey, String, TIMESTAMP
from sqlalchemy import Column, ForeignKey, String, TIMESTAMP, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import JSONB
from enum import Enum
@ -38,6 +38,12 @@ class Environment(
primaryjoin="and_(EnvironmentRole.environment_id == Environment.id, EnvironmentRole.deleted == False)",
)
__table_args__ = (
UniqueConstraint(
"name", "application_id", name="environments_name_application_id_key"
),
)
class ProvisioningStatus(Enum):
PENDING = "pending"
COMPLETED = "completed"

View File

@ -1,5 +1,6 @@
from sqlalchemy import Column, String
from sqlalchemy.orm import relationship
from sqlalchemy.types import ARRAY
from itertools import chain
from atst.models.base import Base
@ -20,7 +21,7 @@ class Portfolio(
name = Column(String, nullable=False)
description = Column(String)
defense_component = Column(
String, nullable=False
ARRAY(String), nullable=False
) # Department of Defense Component
applications = relationship(

View File

@ -12,17 +12,6 @@ from atst.utils import first_or_none
from atst.models.mixins.auditable import record_permission_sets_updates
MEMBER_STATUSES = {
"active": "Active",
"revoked": "Invite revoked",
"expired": "Invite expired",
"error": "Error on invite",
"pending": "Pending",
"unknown": "Unknown errors",
"disabled": "Disabled",
}
class Status(Enum):
ACTIVE = "active"
DISABLED = "disabled"
@ -90,23 +79,23 @@ class PortfolioRole(
@property
def display_status(self):
if self.status == Status.ACTIVE:
return MEMBER_STATUSES["active"]
return "active"
elif self.status == Status.DISABLED:
return MEMBER_STATUSES["disabled"]
return "disabled"
elif self.latest_invitation:
if self.latest_invitation.is_revoked:
return MEMBER_STATUSES["revoked"]
return "invite_revoked"
elif self.latest_invitation.is_rejected_wrong_user:
return MEMBER_STATUSES["error"]
return "invite_error"
elif (
self.latest_invitation.is_rejected_expired
or self.latest_invitation.is_expired
):
return MEMBER_STATUSES["expired"]
return "invite_expired"
else:
return MEMBER_STATUSES["pending"]
return "invite_pending"
else:
return MEMBER_STATUSES["unknown"]
return "unknown"
def has_permission_set(self, perm_set_name):
return first_or_none(

View File

@ -9,7 +9,7 @@ from atst.models.base import Base
import atst.models.types as types
import atst.models.mixins as mixins
from atst.models.attachment import Attachment
from atst.utils.clock import Clock
from pendulum import today
class Status(Enum):
@ -83,26 +83,10 @@ class TaskOrder(Base, mixins.TimestampsMixin):
def is_active(self):
return self.status == Status.ACTIVE
@property
def is_upcoming(self):
return self.status == Status.UPCOMING
@property
def is_expired(self):
return self.status == Status.EXPIRED
@property
def is_unsigned(self):
return self.status == Status.UNSIGNED
@property
def has_begun(self):
return self.start_date is not None and Clock.today() >= self.start_date
@property
def has_ended(self):
return self.start_date is not None and Clock.today() >= self.end_date
@property
def clins_are_completed(self):
return all([len(self.clins), (clin.is_completed for clin in self.clins)])
@ -117,17 +101,17 @@ class TaskOrder(Base, mixins.TimestampsMixin):
@property
def status(self):
today = Clock.today()
todays_date = today(tz="UTC").date()
if not self.is_completed and not self.is_signed:
return Status.DRAFT
elif self.is_completed and not self.is_signed:
return Status.UNSIGNED
elif today < self.start_date:
elif todays_date < self.start_date:
return Status.UPCOMING
elif today >= self.end_date:
elif todays_date > self.end_date:
return Status.EXPIRED
elif self.start_date <= today < self.end_date:
elif self.start_date <= todays_date <= self.end_date:
return Status.ACTIVE
@property
@ -141,39 +125,25 @@ class TaskOrder(Base, mixins.TimestampsMixin):
@property
def days_to_expiration(self):
if self.end_date:
return (self.end_date - Clock.today()).days
return (self.end_date - today(tz="UTC").date()).days
@property
def total_obligated_funds(self):
total = 0
for clin in self.clins:
if clin.obligated_amount is not None:
total += clin.obligated_amount
return total
return sum(
(clin.obligated_amount for clin in self.clins if clin.obligated_amount)
)
@property
def total_contract_amount(self):
total = 0
for clin in self.clins:
if clin.total_amount is not None:
total += clin.total_amount
return total
@property
# TODO delete when we delete task_order_review flow
def budget(self):
return 100000
@property
def balance(self):
# TODO: fix task order -- reimplement using CLINs
# Faked for display purposes
return 50
return sum((clin.total_amount for clin in self.clins if clin.total_amount))
@property
def invoiced_funds(self):
# TODO: implement this using reporting data from the CSP
return self.total_obligated_funds * Decimal(0.75)
if self.is_active:
return self.total_obligated_funds * Decimal(0.75)
else:
return 0
@property
def display_status(self):

View File

@ -1,4 +1,4 @@
from sqlalchemy import String, ForeignKey, Column, Date, Boolean, Table, TIMESTAMP
from sqlalchemy import String, ForeignKey, Column, Date, Table, TIMESTAMP
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.event import listen
@ -67,8 +67,6 @@ class User(
last_login = Column(TIMESTAMP(timezone=True), nullable=True)
last_session_id = Column(UUID(as_uuid=True), nullable=True)
provisional = Column(Boolean)
cloud_id = Column(String)
REQUIRED_FIELDS = [

View File

@ -19,6 +19,7 @@ from werkzeug.exceptions import NotFound
from atst.domain.users import Users
from atst.domain.authnid import AuthenticationContext
from atst.domain.auth import logout as _logout
from atst.domain.exceptions import UnauthenticatedError
from atst.utils.flash import formatted_flash as flash
@ -64,11 +65,15 @@ def catch_all(path):
raise NotFound()
def _client_s_dn():
return request.environ.get("HTTP_X_SSL_CLIENT_S_DN")
def _make_authentication_context():
return AuthenticationContext(
crl_cache=app.crl_cache,
auth_status=request.environ.get("HTTP_X_SSL_CLIENT_VERIFY"),
sdn=request.environ.get("HTTP_X_SSL_CLIENT_S_DN"),
sdn=_client_s_dn(),
cert=request.environ.get("HTTP_X_SSL_CLIENT_CERT"),
)
@ -89,19 +94,24 @@ def current_user_setup(user):
session["user_id"] = user.id
session["last_login"] = user.last_login
app.session_limiter.on_login(user)
app.logger.info(f"authentication succeeded for user with EDIPI {user.dod_id}")
Users.update_last_login(user)
@bp.route("/login-redirect")
def login_redirect():
auth_context = _make_authentication_context()
auth_context.authenticate()
user = auth_context.get_user()
try:
auth_context = _make_authentication_context()
auth_context.authenticate()
if user.provisional:
Users.finalize(user)
user = auth_context.get_user()
current_user_setup(user)
except UnauthenticatedError as err:
app.logger.info(
f"authentication failed for subject distinguished name {_client_s_dn()}"
)
raise err
current_user_setup(user)
return redirect(redirect_after_login_url())

View File

@ -1,8 +1,7 @@
from flask import redirect, render_template, request as http_request, url_for, g
from flask import redirect, render_template, request as http_request, url_for
from .blueprint import applications_bp
from atst.domain.applications import Applications
from atst.domain.portfolios import Portfolios
from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.models.permissions import Permissions
@ -12,6 +11,7 @@ from atst.routes.applications.settings import (
get_new_member_form,
handle_create_member,
handle_update_member,
handle_update_application,
)
@ -64,17 +64,9 @@ def create_or_update_new_application_step_1(portfolio_id=None, application_id=No
form = get_new_application_form(
{**http_request.form}, NameAndDescriptionForm, application_id
)
application = handle_update_application(form, application_id, portfolio_id)
if form.validate():
application = None
if application_id:
application = Applications.get(application_id)
application = Applications.update(application, form.data)
flash("application_updated", application_name=application.name)
else:
portfolio = Portfolios.get_for_update(portfolio_id)
application = Applications.create(g.current_user, portfolio, **form.data)
flash("application_created", application_name=application.name)
if application:
return redirect(
url_for(
"applications.update_new_application_step_2",

View File

@ -1,4 +1,10 @@
from flask import redirect, render_template, request as http_request, url_for, g
from flask import (
redirect,
render_template,
request as http_request,
url_for,
g,
)
from .blueprint import applications_bp
from atst.domain.exceptions import AlreadyExistsError
@ -10,6 +16,7 @@ from atst.domain.csp.cloud import GeneralCSPException
from atst.domain.common import Paginator
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.invitations import ApplicationInvitations
from atst.domain.portfolios import Portfolios
from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
@ -245,16 +252,59 @@ def handle_update_member(application_id, application_role_id, form_data):
# TODO: flash error message
def handle_update_environment(form, application=None, environment=None):
if form.validate():
try:
if environment:
environment = Environments.update(
environment=environment, name=form.name.data
)
flash("application_environments_updated")
else:
environment = Environments.create(
g.current_user, application=application, name=form.name.data
)
flash("environment_added", environment_name=form.name.data)
return environment
except AlreadyExistsError:
flash("application_environments_name_error", name=form.name.data)
return False
else:
return False
def handle_update_application(form, application_id=None, portfolio_id=None):
if form.validate():
application = None
try:
if application_id:
application = Applications.get(application_id)
application = Applications.update(application, form.data)
flash("application_updated", application_name=application.name)
else:
portfolio = Portfolios.get_for_update(portfolio_id)
application = Applications.create(
g.current_user, portfolio, **form.data
)
flash("application_created", application_name=application.name)
return application
except AlreadyExistsError:
flash("application_name_error", name=form.data["name"])
return False
@applications_bp.route("/applications/<application_id>/settings")
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
def settings(application_id):
application = Applications.get(application_id)
return render_settings_page(
application=application,
active_toggler=http_request.args.get("active_toggler"),
active_toggler_section=http_request.args.get("active_toggler_section"),
)
return render_settings_page(application=application,)
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
@ -264,31 +314,21 @@ def update_environment(environment_id):
application = environment.application
env_form = EditEnvironmentForm(obj=environment, formdata=http_request.form)
updated_environment = handle_update_environment(
form=env_form, application=application, environment=environment
)
if env_form.validate():
Environments.update(environment=environment, name=env_form.name.data)
flash("application_environments_updated")
if updated_environment:
return redirect(
url_for(
"applications.settings",
application_id=application.id,
fragment="application-environments",
_anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
)
)
else:
return (
render_settings_page(
application=application,
active_toggler=environment.id,
active_toggler_section="edit",
),
400,
)
return (render_settings_page(application=application, show_flash=True), 400)
@applications_bp.route(
@ -298,14 +338,9 @@ def update_environment(environment_id):
def new_environment(application_id):
application = Applications.get(application_id)
env_form = EditEnvironmentForm(formdata=http_request.form)
environment = handle_update_environment(form=env_form, application=application)
if env_form.validate():
Environments.create(
g.current_user, application=application, name=env_form.name.data
)
flash("environment_added", environment_name=env_form.data["name"])
if environment:
return redirect(
url_for(
"applications.settings",
@ -315,7 +350,7 @@ def new_environment(application_id):
)
)
else:
return (render_settings_page(application=application), 400)
return (render_settings_page(application=application, show_flash=True), 400)
@applications_bp.route("/applications/<application_id>/edit", methods=["POST"])
@ -323,10 +358,9 @@ def new_environment(application_id):
def update(application_id):
application = Applications.get(application_id)
form = NameAndDescriptionForm(http_request.form)
if form.validate():
application_data = form.data
Applications.update(application, application_data)
updated_application = handle_update_application(form, application_id)
if updated_application:
return redirect(
url_for(
"applications.portfolio_applications",
@ -334,22 +368,10 @@ def update(application_id):
)
)
else:
return render_settings_page(application=application, application_form=form)
@applications_bp.route("/applications/<application_id>/delete", methods=["POST"])
@user_can(Permissions.DELETE_APPLICATION, message="delete application")
def delete(application_id):
application = Applications.get(application_id)
Applications.delete(application)
flash("application_deleted", application_name=application.name)
return redirect(
url_for(
"applications.portfolio_applications", portfolio_id=application.portfolio_id
return (
render_settings_page(application=application, show_flash=True),
400,
)
)
@applications_bp.route("/environments/<environment_id>/delete", methods=["POST"])

View File

@ -17,63 +17,51 @@ from atst.utils.flash import formatted_flash as flash
from atst.domain.exceptions import UnauthorizedError
def permission_str(member, edit_perm_set, view_perm_set):
if member.has_permission_set(edit_perm_set):
return edit_perm_set
else:
return view_perm_set
def serialize_member_form_data(member):
return {
"member_name": member.full_name,
"member_id": member.id,
"perms_app_mgmt": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
def filter_perm_sets_data(member):
perm_sets_data = {
"perms_portfolio_mgmt": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
),
"perms_funding": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_FUNDING,
PermissionSets.VIEW_PORTFOLIO_FUNDING,
"perms_app_mgmt": bool(
member.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
)
),
"perms_reporting": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_REPORTS,
PermissionSets.VIEW_PORTFOLIO_REPORTS,
"perms_funding": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING)
),
"perms_portfolio_mgmt": permission_str(
member,
PermissionSets.EDIT_PORTFOLIO_ADMIN,
PermissionSets.VIEW_PORTFOLIO_ADMIN,
"perms_reporting": bool(
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
),
}
return perm_sets_data
def get_members_data(portfolio):
members = sorted(
[serialize_member_form_data(member) for member in portfolio.members],
key=lambda member: member["member_name"],
)
for member in members:
if member["member_id"] == portfolio.owner_role.id:
ppoc = member
members.remove(member)
members.insert(0, ppoc)
return members
def filter_members_data(members_list, portfolio):
members_data = []
for member in members_list:
members_data.append(
{
"role_id": member.id,
"user_name": member.user_name,
"permission_sets": filter_perm_sets_data(member),
"status": member.display_status,
"ppoc": PermissionSets.PORTFOLIO_POC in member.permission_sets,
# add in stuff here for forms
}
)
return sorted(members_data, key=lambda member: member["user_name"])
def render_admin_page(portfolio, form=None):
pagination_opts = Paginator.get_pagination_opts(http_request)
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
members_data = get_members_data(portfolio)
portfolio_form = PortfolioForm(data={"name": portfolio.name})
member_perms_form = member_forms.MembersPermissionsForm(
data={"members_permissions": members_data}
)
portfolio_form = PortfolioForm(obj=portfolio)
member_list = portfolio.members
assign_ppoc_form = member_forms.AssignPPOCForm()
for pf_role in portfolio.roles:
if pf_role.user != portfolio.owner and pf_role.is_active:
assign_ppoc_form.role_id.choices += [(pf_role.id, pf_role.full_name)]
@ -87,13 +75,12 @@ def render_admin_page(portfolio, form=None):
"portfolios/admin.html",
form=form,
portfolio_form=portfolio_form,
member_perms_form=member_perms_form,
member_form=member_forms.NewForm(),
members=filter_members_data(member_list, portfolio),
new_manager_form=member_forms.NewForm(),
assign_ppoc_form=assign_ppoc_form,
portfolio=portfolio,
audit_events=audit_events,
user=g.current_user,
ppoc_id=members_data[0].get("member_id"),
current_member_id=current_member_id,
applications_count=len(portfolio.applications),
)
@ -106,34 +93,6 @@ def admin(portfolio_id):
return render_admin_page(portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/admin", methods=["POST"])
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="view portfolio admin page")
def edit_members(portfolio_id):
portfolio = Portfolios.get_for_update(portfolio_id)
member_perms_form = member_forms.MembersPermissionsForm(http_request.form)
if member_perms_form.validate():
for subform in member_perms_form.members_permissions:
member_id = subform.member_id.data
member = PortfolioRoles.get_by_id(member_id)
if member is not portfolio.owner_role:
new_perm_set = subform.data["permission_sets"]
PortfolioRoles.update(member, new_perm_set)
flash("update_portfolio_members", portfolio=portfolio)
return redirect(
url_for(
"portfolios.admin",
portfolio_id=portfolio_id,
fragment="portfolio-members",
_anchor="portfolio-members",
)
)
else:
return render_admin_page(portfolio)
@portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
@user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
def update_ppoc(portfolio_id):

View File

@ -56,13 +56,3 @@ def reports(portfolio_id):
monthly_spending=Reports.monthly_spending(portfolio),
retrieved=datetime.now(), # mocked datetime of reporting data retrival
)
@portfolios_bp.route("/portfolios/<portfolio_id>/destroy", methods=["POST"])
@user_can(Permissions.ARCHIVE_PORTFOLIO, message="archive portfolio")
def delete_portfolio(portfolio_id):
Portfolios.delete(portfolio=g.portfolio)
flash("portfolio_deleted", portfolio_name=g.portfolio.name)
return redirect(url_for("atst.home"))

View File

@ -79,7 +79,7 @@ def invite_member(portfolio_id):
if form.validate():
try:
invite = Portfolios.invite(portfolio, g.current_user, form.update_data)
invite = Portfolios.invite(portfolio, g.current_user, form.data)
send_portfolio_invitation(
invitee_email=invite.email,
inviter_name=g.current_user.full_name,

View File

@ -8,16 +8,16 @@ from atst.forms.task_order import SignatureForm
from atst.models import Permissions
@task_orders_bp.route("/task_orders/<task_order_id>/review")
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="review task order details")
def review_task_order(task_order_id):
@task_orders_bp.route("/task_orders/<task_order_id>")
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order details")
def view_task_order(task_order_id):
task_order = TaskOrders.get(task_order_id)
if task_order.is_draft:
return redirect(url_for("task_orders.edit", task_order_id=task_order.id))
else:
signature_form = SignatureForm()
return render_template(
"task_orders/review.html",
"task_orders/view.html",
task_order=task_order,
signature_form=signature_form,
)

View File

@ -1,5 +1,10 @@
import re
from sqlalchemy.exc import IntegrityError
from atst.database import db
from atst.domain.exceptions import AlreadyExistsError
def first_or_none(predicate, lst):
return next((x for x in lst if predicate(x)), None)
@ -23,3 +28,11 @@ def camel_to_snake(camel_cased):
def pick(keys, dct):
_keys = set(keys)
return {k: v for (k, v) in dct.items() if k in _keys}
def commit_or_raise_already_exists_error(message):
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
raise AlreadyExistsError(message)

View File

@ -1,11 +0,0 @@
import pendulum
class Clock(object):
@classmethod
def today(cls, tz="UTC"):
return pendulum.today(tz=tz).date()
@classmethod
def now(cls, tz="UTC"):
return pendulum.now(tz=tz)

View File

@ -1,204 +1,171 @@
from flask import flash, render_template_string
from flask import flash
from atst.utils.localization import translate
MESSAGES = {
"portfolio_deleted": {
"title_template": "Portfolio has been deleted",
"message_template": "Portfolio '{{portfolio_name}}' has been deleted",
"category": "success",
},
"application_created": {
"title_template": translate("flash.application.created.title"),
"message_template": """
{{ "flash.application.created.message" | translate({"application_name": application_name}) }}
""",
"title": "flash.application.created.title",
"message": "flash.application.created.message",
"category": "success",
},
"application_updated": {
"title_template": translate("flash.success"),
"message_template": """
{{ "flash.application.updated" | translate({"application_name": application_name}) }}
""",
"title": "flash.success",
"message": "flash.application.updated",
"category": "success",
},
"application_deleted": {
"title_template": translate("flash.success"),
"message_template": """
{{ "flash.application.deleted" | translate({"application_name": application_name}) }}
<a href="#">{{ "common.undo" | translate }}</a>
""",
"category": "success",
"application_environments_name_error": {
"title": None,
"message": "flash.application.env_name_error.message",
"category": "error",
},
"application_environments_updated": {
"title_template": "Application environments updated",
"message_template": "Application environments have been updated",
"title": "flash.environment.updated.title",
"message": "flash.environment.updated.message",
"category": "success",
},
"application_invite_error": {
"title_template": "Application invitation error",
"message_template": "There was an error processing the invitation for {{ user_name }} from {{ application_name }}",
"title": "flash.application_invite.error.title",
"message": "flash.application_invite.error.message",
"category": "error",
},
"application_invite_resent": {
"title_template": "Application invitation resent",
"message_template": "You have successfully resent the invite for {{ user_name }} from {{ application_name }}",
"title": "flash.application_invite.resent.title",
"message": "flash.application_invite.resent.message",
"category": "success",
},
"application_invite_revoked": {
"title_template": "Application invitation revoked",
"message_template": "You have successfully revoked the invite for {{ user_name }} from {{ application_name }}",
"title": "flash.application_invite.revoked.title",
"message": "flash.application_invite.revoked.message",
"category": "success",
},
"application_member_removed": {
"title_template": "Team member removed from application",
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
"title": "flash.application_member.removed.title",
"message": "flash.application_member.removed.message",
"category": "success",
},
"application_member_update_error": {
"title_template": "{{ user_name }} could not be updated",
"message_template": "An unexpected problem occurred with your request, please try again. If the problem persists, contact an administrator.",
"title": "flash.application_member.update_error.title",
"message": "flash.application_member.update_error.message",
"category": "error",
},
"application_member_updated": {
"title_template": "Team member updated",
"message_template": "You have successfully updated the permissions for {{ user_name }}",
"title": "flash.application_member.updated.title",
"message": "flash.application_member.updated.message",
"category": "success",
},
"application_name_error": {
"title": None,
"message": "flash.application.name_error.message",
"category": "error",
},
"ccpo_user_added": {
"title_template": translate("flash.success"),
"message_template": "You have successfully given {{ user_name }} CCPO permissions.",
"title": "flash.success",
"message": "flash.ccpo_user.added.message",
"category": "success",
},
"ccpo_user_not_found": {
"title_template": translate("ccpo.form.user_not_found_title"),
"message_template": translate("ccpo.form.user_not_found_text"),
"title": "ccpo.form.user_not_found_title",
"message": "ccpo.form.user_not_found_text",
"category": "info",
},
"ccpo_user_removed": {
"title_template": translate("flash.success"),
"message_template": "You have successfully removed {{ user_name }}'s CCPO permissions.",
"title": "flash.success",
"message": "flash.ccpo_user.removed.message",
"category": "success",
},
"environment_added": {
"title_template": translate("flash.success"),
"message_template": """
{{ "flash.environment_added" | translate({ "env_name": environment_name }) }}
""",
"title": "flash.success",
"message": "flash.environment_added",
"category": "success",
},
"environment_deleted": {
"title_template": "{{ environment_name }} deleted",
"message_template": 'The environment "{{ environment_name }}" has been deleted',
"title": "flash.environment.deleted.title",
"message": "flash.environment.deleted.message",
"category": "success",
},
"form_errors": {
"title_template": "There were some errors",
"message_template": "<p>Please see below.</p>",
"title": "flash.form.errors.title",
"message": "flash.form.errors.message",
"category": "error",
},
"insufficient_funds": {
"title_template": "Insufficient Funds",
"message_template": "",
"title": "flash.task_order.insufficient_funds.title",
"message": "",
"category": "warning",
},
"logged_out": {
"title_template": translate("flash.logged_out"),
"message_template": """
You've been logged out.
""",
"title": "flash.logged_out.title",
"message": "flash.logged_out.message",
"category": "info",
},
"login_next": {
"title_template": translate("flash.login_required_title"),
"message_template": translate("flash.login_required_message"),
"title": "flash.login_required_title",
"message": "flash.login_required_message",
"category": "warning",
},
"new_application_member": {
"title_template": """{{ "flash.new_application_member.title" | translate({ "user_name": user_name }) }}""",
"message_template": """
<p>{{ "flash.new_application_member.message" | translate({ "user_name": user_name }) }}</p>
""",
"title": "flash.new_application_member.title",
"message": "flash.new_application_member.message",
"category": "success",
},
"new_portfolio_member": {
"title_template": translate("flash.success"),
"message_template": """
<p>{{ "flash.new_portfolio_member" | translate({ "user_name": user_name }) }}</p>
""",
"title": "flash.success",
"message": "flash.new_portfolio_member",
"category": "success",
},
"portfolio_member_removed": {
"title_template": translate("flash.deleted_member"),
"message_template": """
{{ "flash.delete_member_success" | translate({ "member_name": member_name }) }}
""",
"title": "flash.deleted_member",
"message": "flash.delete_member_success",
"category": "success",
},
"primary_point_of_contact_changed": {
"title_template": translate("flash.new_ppoc_title"),
"message_template": """{{ "flash.new_ppoc_message" | translate({ "ppoc_name": ppoc_name }) }}""",
"title": "flash.new_ppoc_title",
"message": "flash.new_ppoc_message",
"category": "success",
},
"resend_portfolio_invitation": {
"title_template": "Invitation resent",
"message_template": """
<p>Successfully sent a new invitation to {{ user_name }}.</p>
""",
"title": "flash.portfolio_invite.resent.title",
"message": "flash.portfolio_invite.resent.message",
"category": "success",
},
"revoked_portfolio_access": {
"title_template": "Removed portfolio access",
"message_template": """
<p>Portfolio access successfully removed from {{ member_name }}.</p>
""",
"title": "flash.portfolio_member.revoked.title",
"message": "flash.portfolio_member.revoked.message",
"category": "success",
},
"session_expired": {
"title_template": "Session Expired",
"message_template": """
Your session expired due to inactivity. Please log in again to continue.
""",
"title": "flash.session_expired.title",
"message": "flash.session_expired.message",
"category": "error",
},
"task_order_draft": {
"title_template": translate("task_orders.form.draft_alert_title"),
"message_template": translate("task_orders.form.draft_alert_message"),
"title": "task_orders.form.draft_alert_title",
"message": "task_orders.form.draft_alert_message",
"category": "warning",
},
"task_order_number_error": {
"title_template": "",
"message_template": """{{ 'flash.task_order_number_error.message' | translate({ 'to_number': to_number }) }}""",
"title": None,
"message": "flash.task_order_number_error.message",
"category": "error",
},
"task_order_submitted": {
"title_template": "Your Task Order has been uploaded successfully.",
"message_template": """
Your task order form for {{ task_order.portfolio_name }} has been submitted.
""",
"category": "success",
},
"update_portfolio_members": {
"title_template": "Success!",
"message_template": """
<p>You have successfully updated access permissions for members of {{ portfolio.name }}.</p>
""",
"title": "flash.task_order.submitted.title",
"message": "flash.task_order.submitted.message",
"category": "success",
},
"updated_application_team_settings": {
"title_template": translate("flash.success"),
"message_template": """
<p>{{ "flash.updated_application_team_settings" | translate({"application_name": application_name}) }}</p>
""",
"title": "flash.success",
"message": "flash.updated_application_team_settings",
"category": "success",
},
"user_must_complete_profile": {
"title_template": "You must complete your profile",
"message_template": "<p>Before continuing, you must complete your profile</p>",
"title": "flash.user.complete_profile.title",
"message": "flash.user.complete_profile.message",
"category": "info",
},
"user_updated": {
"title_template": "User information updated.",
"message_template": "",
"title": "flash.user.updated.title",
"message": None,
"category": "success",
},
}
@ -206,9 +173,11 @@ MESSAGES = {
def formatted_flash(message_name, **message_args):
config = MESSAGES[message_name]
title = render_template_string(config["title_template"], **message_args)
message = render_template_string(config["message_template"], **message_args)
actions = None
if "actions" in config:
actions = render_template_string(config["actions"], **message_args)
title = translate(config["title"], message_args) if config["title"] else None
message = translate(config["message"], message_args) if config["message"] else None
actions = (
translate(config["actions"], message_args) if config.get("actions") else None
)
flash({"title": title, "message": message, "actions": actions}, config["category"])

View File

@ -2,16 +2,22 @@ import datetime
import json
import logging
from flask import g, request, has_request_context
from flask import g, request, has_request_context, session
class RequestContextFilter(logging.Filter):
def filter(self, record):
if has_request_context():
if getattr(g, "current_user", None):
record.user_id = str(g.current_user.id)
record.dod_edipi = g.current_user.dod_id
user_id = session.get("user_id")
if user_id:
record.user_id = str(user_id)
record.logged_in = True
else:
record.logged_in = False
if request.environ.get("HTTP_X_REQUEST_ID"):
record.request_id = request.environ.get("HTTP_X_REQUEST_ID")
@ -30,6 +36,7 @@ class JsonFormatter(logging.Formatter):
("request_id", lambda r: r.__dict__.get("request_id")),
("user_id", lambda r: r.__dict__.get("user_id")),
("dod_edipi", lambda r: r.__dict__.get("dod_edipi")),
("logged_in", lambda r: r.__dict__.get("logged_in")),
("severity", lambda r: r.levelname),
("tags", lambda r: r.__dict__.get("tags")),
("audit_event", lambda r: r.__dict__.get("audit_event")),
@ -44,7 +51,7 @@ class JsonFormatter(logging.Formatter):
for field, func in self._DEFAULT_RECORD_FIELDS:
result = func(record)
if result:
if result is not None:
message_dict[field] = result
if record.args:

View File

@ -3,6 +3,7 @@ ASSETS_URL
AZURE_ACCOUNT_NAME
AZURE_STORAGE_KEY
AZURE_TO_BUCKET_NAME
AZURE_POLICY_LOCATION=policies
BLOB_STORAGE_URL=http://localhost:8000/
CAC_URL = http://localhost:8000/login-redirect
CA_CHAIN = ssl/server-certs/ca-chain.pem
@ -39,6 +40,7 @@ REDIS_USER
SECRET_KEY = change_me_into_something_secret
SERVER_NAME
SESSION_COOKIE_NAME=atat
SESSION_COOKIE_DOMAIN
SESSION_TYPE = redis
SESSION_USE_SIGNER = True
SQLALCHEMY_ECHO = False

View File

@ -30,6 +30,7 @@ data:
PGUSER: atat_master@atat-db
REDIS_HOST: atat.redis.cache.windows.net:6380
REDIS_TLS: "true"
SESSION_COOKIE_DOMAIN: atat.code.mil
STATIC_URL: https://atat-cdn.azureedge.net/static/
TZ: UTC
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini

View File

@ -45,7 +45,7 @@ data:
include /etc/nginx/snippets/ssl.conf;
location /login-redirect {
return 301 https://auth-azure.atat.code.mil$request_uri;
return 301 https://${AUTH_DOMAIN}$request_uri;
}
location /login-dev {
try_files $uri @appbasicauth;
@ -82,7 +82,7 @@ data:
include /etc/nginx/snippets/ssl.conf;
location / {
return 301 https://azure.atat.code.mil$request_uri;
return 301 https://${MAIN_DOMAIN}$request_uri;
}
location /login-redirect {
try_files $uri @app;

View File

@ -0,0 +1,22 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: atst-worker-envvars
data:
CELERY_DEFAULT_QUEUE: celery-staging
SERVER_NAME: staging.atat.code.mil
FLASK_ENV: staging
---
apiVersion: v1
kind: ConfigMap
metadata:
name: atst-envvars
data:
ASSETS_URL: https://atat-cdn-staging.azureedge.net/
CDN_ORIGIN: https://staging.atat.code.mil
CELERY_DEFAULT_QUEUE: celery-staging
FLASK_ENV: staging
STATIC_URL: https://atat-cdn-staging.azureedge.net/static/
PGHOST: cloudzero-dev-sql.postgres.database.azure.com
REDIS_HOST: cloudzero-dev-redis.redis.cache.windows.net:6380

View File

@ -0,0 +1,62 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: atst
spec:
template:
spec:
volumes:
- name: nginx-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "dhparam4096;cert;cert"
- name: flask-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "AZURE-STORAGE-KEY;MAIL-PASSWORD;PGPASSWORD;REDIS-PASSWORD;SECRET-KEY"
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: atst-worker
spec:
template:
spec:
volumes:
- name: flask-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "AZURE-STORAGE-KEY;MAIL-PASSWORD;PGPASSWORD;REDIS-PASSWORD;SECRET-KEY"
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: atst-beat
spec:
template:
spec:
volumes:
- name: flask-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "AZURE-STORAGE-KEY;MAIL-PASSWORD;PGPASSWORD;REDIS-PASSWORD;SECRET-KEY"
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: crls
spec:
jobTemplate:
spec:
template:
spec:
volumes:
- name: flask-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "AZURE-STORAGE-KEY;MAIL-PASSWORD;PGPASSWORD;REDIS-PASSWORD;SECRET-KEY"

View File

@ -0,0 +1,12 @@
- op: replace
path: /spec/template/spec/containers/1/ports/0/containerPort
value: 9342
- op: replace
path: /spec/template/spec/containers/1/ports/1/containerPort
value: 9442
- op: replace
path: /spec/template/spec/containers/1/ports/2/containerPort
value: 9343
- op: replace
path: /spec/template/spec/containers/1/ports/3/containerPort
value: 9443

View File

@ -0,0 +1,18 @@
namespace: staging
bases:
- ../../azure/
resources:
- namespace.yml
- reset-cron-job.yml
patchesStrategicMerge:
- replica_count.yml
- ports.yml
- envvars.yml
- flex_vol.yml
patchesJson6902:
- target:
group: extensions
version: v1beta1
kind: Deployment
name: atst
path: json_ports.yml

View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: staging

View File

@ -0,0 +1,28 @@
---
apiVersion: v1
kind: Service
metadata:
name: atst-main
spec:
loadBalancerIP: ""
ports:
- port: 80
targetPort: 9342
name: http
- port: 443
targetPort: 9442
name: https
---
apiVersion: v1
kind: Service
metadata:
name: atst-auth
spec:
loadBalancerIP: ""
ports:
- port: 80
targetPort: 9343
name: http
- port: 443
targetPort: 9443
name: https

View File

@ -0,0 +1,14 @@
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: atst
spec:
replicas: 2
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: atst-worker
spec:
replicas: 1

View File

@ -0,0 +1,46 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: reset-db
namespace: atat
spec:
schedule: "0 4 * * *"
concurrencyPolicy: Replace
successfulJobsHistoryLimit: 1
jobTemplate:
spec:
template:
metadata:
labels:
app: atst
role: reset-db
aadpodidbinding: atat-kv-id-binding
spec:
restartPolicy: OnFailure
containers:
- name: reset
image: $CONTAINER_IMAGE
command: [
"/bin/sh", "-c"
]
args: [
"/opt/atat/atst/.venv/bin/python",
"/opt/atat/atst/script/reset_database.py"
]
envFrom:
- configMapRef:
name: atst-worker-envvars
volumeMounts:
- name: flask-secret
mountPath: "/config"
volumes:
- name: flask-secret
flexVolume:
driver: "azure/kv"
options:
usepodidentity: "true"
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
keyvaultobjecttypes: "secret;secret;secret;secret;key"
tenantid: $TENANT_ID

View File

@ -3,6 +3,7 @@ bases:
- ../../azure/
resources:
- namespace.yml
- reset-cron-job.yml
patchesStrategicMerge:
- replica_count.yml
- ports.yml

View File

@ -0,0 +1,46 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: reset-db
namespace: atat
spec:
schedule: "0 4 * * *"
concurrencyPolicy: Replace
successfulJobsHistoryLimit: 1
jobTemplate:
spec:
template:
metadata:
labels:
app: atst
role: reset-db
aadpodidbinding: atat-kv-id-binding
spec:
restartPolicy: OnFailure
containers:
- name: reset
image: $CONTAINER_IMAGE
command: [
"/bin/sh", "-c"
]
args: [
"/opt/atat/atst/.venv/bin/python",
"/opt/atat/atst/script/reset_database.py"
]
envFrom:
- configMapRef:
name: atst-worker-envvars
volumeMounts:
- name: flask-secret
mountPath: "/config"
volumes:
- name: flask-secret
flexVolume:
driver: "azure/kv"
options:
usepodidentity: "true"
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
keyvaultobjecttypes: "secret;secret;secret;secret;key"
tenantid: $TENANT_ID

View File

@ -1,4 +1,5 @@
import { emitFieldChange } from '../lib/emitters'
import escape from '../lib/escape'
import optionsinput from './options_input'
import textinput from './text_input'
import clindollaramount from './clin_dollar_amount'
@ -99,7 +100,7 @@ export default {
computed: {
clinTitle: function() {
if (!!this.clinNumber) {
return `CLIN ${this.clinNumber}`
return escape(`CLIN ${this.clinNumber}`)
} else {
return `CLIN`
}

View File

@ -0,0 +1,21 @@
import escape from '../escape'
describe('escape', () => {
const htmlEscapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
}
it('should escape each character', () => {
for (let [char, escapedChar] of Object.entries(htmlEscapes)) {
expect(escape(char)).toBe(escapedChar)
}
})
it('should escape multiple characters', () => {
expect(escape('& and < and > and " and \' and /')).toBe(
'&amp; and &lt; and &gt; and &quot; and &#x27; and &#x2F;'
)
})
})

20
js/lib/escape.js Normal file
View File

@ -0,0 +1,20 @@
// https://stackoverflow.com/a/6020820
// List of HTML entities for escaping.
const htmlEscapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
}
const htmlEscaper = /[&<>"'\/]/g
// Escape a string for HTML interpolation.
const escape = string => {
return ('' + string).replace(htmlEscaper, match => htmlEscapes[match])
}
export default escape

View File

@ -0,0 +1,40 @@
{
"definitionPoint": "portfolio",
"policyDefinition": {
"properties": {
"displayName": "Allowed resource types",
"policyType": "Custom",
"mode": "Indexed",
"description": "This policy enables you to specify the resource types that your organization can deploy.",
"parameters": {
"listOfResourceTypesAllowed": {
"type": "Array",
"metadata": {
"description": "The list of resource types that can be deployed.",
"displayName": "Allowed resource types",
"strongType": "resourceTypes"
}
}
},
"policyRule": {
"if": {
"not": {
"field": "type",
"in": "[parameters('listOfResourceTypesAllowed')]"
}
},
"then": {
"effect": "deny"
}
}
},
"type": "Microsoft.Authorization/policyDefinitions"
},
"parameters": {
"listOfResourceTypesAllowed": {
"value": [
"Microsoft.Cache"
]
}
}
}

View File

@ -0,0 +1,51 @@
{
"definitionPoint": "portfolio",
"policyDefinition": {
"properties": {
"displayName": "Custom - Region Restriction",
"policyType": "Custom",
"mode": "Indexed",
"parameters": {
"listOfAllowedLocations": {
"type": "Array",
"metadata": {
"displayName": "Allowed locations",
"description": "The list of locations that can be specified when deploying resources.",
"strongType": "location"
}
}
},
"policyRule": {
"if": {
"allOf": [
{
"field": "location",
"notIn": "[parameters('listOfAllowedLocations')]"
},
{
"field": "location",
"notEquals": "global"
},
{
"field": "type",
"notEquals": "Microsoft.AzureActiveDirectory/b2cDirectories"
}
]
},
"then": {
"effect": "Deny"
}
}
},
"type": "Microsoft.Authorization/policyDefinitions"
},
"parameters": {
"listOfAllowedLocations": {
"value": [
"eastus",
"southcentralus",
"westus"
]
}
}
}

View File

@ -1,4 +1,5 @@
#!/bin/bash
set -e
# script/cibuild: Run CI related checks and tests

View File

@ -26,28 +26,16 @@ fi
## Main
if [ "${RUN_PYTHON_TESTS}" = "true" ]; then
python_test_status=0
set +e
output_divider "Lint Python files"
run_python_lint "${PYTHON_FILES}"
((python_test_status+=$?))
output_divider "Perform static analysis on Python files"
run_python_static_analysis "${PYTHON_FILES}"
((python_test_status+=$?))
output_divider "Perform type checking on Python files"
run_python_typecheck
((python_test_status+=$?))
output_divider "Run Python unit test suite"
run_python_unit_tests "${PYTHON_FILES}"
((python_test_status+=$?))
if [ "${python_test_status}" != "0" ]; then
warning "Failed to pass one or more Python checks"
exit ${python_test_status}
fi
set -e
fi
if [ "${RUN_JS_TESTS}" = "true" ]; then

View File

@ -72,15 +72,18 @@ $CONTAINER_IMAGE \
# Use curl to wait for application container to become available
docker pull curlimages/curl:latest
echo "Waiting for application container to become available"
docker run --network atat \
curlimages/curl:latest \
curl --connect-timeout 3 \
curl \
--silent \
--connect-timeout 3 \
--max-time 5 \
--retry $CONTAINER_TIMEOUT \
--retry-connrefused \
--retry-delay 1 \
--retry-max-time $CONTAINER_TIMEOUT \
test-atat:8000
test-atat:8000 >/dev/null
# Run Ghost Inspector tests
docker pull ghostinspector/test-runner-standalone:latest

View File

@ -34,7 +34,7 @@ from atst.routes.dev import _DEV_USERS as DEV_USERS
from atst.utils import pick
from tests.factories import (
random_service_branch,
random_defense_component,
TaskOrderFactory,
CLINFactory,
AttachmentFactory,
@ -159,7 +159,7 @@ def get_users():
def add_members_to_portfolio(portfolio):
for user_data in PORTFOLIO_USERS:
invite = Portfolios.invite(portfolio, portfolio.owner, user_data)
invite = Portfolios.invite(portfolio, portfolio.owner, {"user_data": user_data})
profile = {
k: user_data[k] for k in user_data if k not in ["dod_id", "permission_sets"]
}
@ -308,7 +308,7 @@ def create_demo_portfolio(name, data):
portfolio = Portfolios.create(
user=portfolio_owner,
portfolio_attrs={"name": name, "defense_component": random_service_branch()},
portfolio_attrs={"name": name, "defense_component": random_defense_component()},
)
add_task_orders_to_portfolio(portfolio)
@ -336,7 +336,7 @@ def seed_db():
user=amanda,
portfolio_attrs={
"name": "TIE Interceptor",
"defense_component": random_service_branch(),
"defense_component": random_defense_component(),
},
)
add_task_orders_to_portfolio(tie_interceptor)
@ -347,7 +347,7 @@ def seed_db():
user=amanda,
portfolio_attrs={
"name": "TIE Fighter",
"defense_component": random_service_branch(),
"defense_component": random_defense_component(),
},
)
add_task_orders_to_portfolio(tie_fighter)
@ -363,7 +363,7 @@ def seed_db():
user=user,
portfolio_attrs={
"name": ship,
"defense_component": random_service_branch(),
"defense_component": random_defense_component(),
},
)
add_task_orders_to_portfolio(portfolio)

View File

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

View File

@ -1,27 +0,0 @@
-----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-----

View File

@ -1 +0,0 @@
F4D74F1607DD3C83

View File

@ -1,18 +0,0 @@
#!/bin/bash
# Generate the root (GIVE IT A PASSWORD IF YOU'RE NOT AUTOMATING SIGNING!):
echo 'MAKING CA'
openssl genrsa -out certificate-authority/ca.key 2048
openssl req -new -x509 -days 7300 -key certificate-authority/ca.key -sha256 -extensions v3_ca -out certificate-authority/ca.crt
# Generate the domain key:
openssl genrsa -out server-certs/dev.cac.atat.codes.key 2048
echo 'MAKING CSR'
# Generate the certificate signing request
openssl req -nodes -sha256 -new -key server-certs/dev.cac.atat.codes.key -out server-certs/dev.cac.atat.codes.csr -reqexts SAN -config <(cat req.cnf <(printf "[SAN]\nsubjectAltName=DNS.1:dev.cac.atat.codes,DNS.2:cac.atat.codes,DNS.3:backend"))
# Sign the request with your root key
openssl x509 -sha256 -req -in server-certs/dev.cac.atat.codes.csr -CA certificate-authority/ca.crt -CAkey certificate-authority/ca.key -CAcreateserial -out server-certs/dev.cac.atat.codes.crt -days 7300 -extfile <(cat req.cnf <(printf "[SAN]\nsubjectAltName=DNS.1:dev.cac.atat.codes,DNS.2:cac.atat.codes,DNS.3:backend")) -extensions SAN
# Check your homework:
openssl verify -CAfile certificate-authority/ca.crt server-certs/dev.cac.atat.codes.crt

View File

@ -38,6 +38,7 @@
@import "components/dod_login_notice.scss";
@import "components/sticky_cta.scss";
@import "components/error_page.scss";
@import "components/member_form.scss";
@import "sections/login";
@import "sections/home";

View File

@ -3,8 +3,7 @@
background-color: $color-white;
border-top: 1px solid $color-gray-lightest;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-direction: row-reverse;
align-items: center;
padding: $gap * 1.5;
position: fixed;
@ -15,19 +14,7 @@
color: $color-gray-dark;
font-size: 1.5rem;
&__info {
flex-grow: 1;
&__login {
padding-left: 0.8rem;
&__link {
margin: (-$gap * 2) (-$gap);
font-weight: normal;
.icon--footer {
@include icon-size(16);
margin: 0rem 0.8rem 0rem 0rem;
}
}
}
}

View File

@ -1,7 +1,8 @@
// Form Grid
.form-row {
margin: ($gap * 4) 0;
&--separated {
&--bordered {
border-bottom: $color-gray-lighter 1px solid;
}
@ -202,3 +203,7 @@
display: inline;
}
}
.form-container__half {
max-width: 46rem;
}

View File

@ -4,18 +4,4 @@
height: auto;
box-shadow: $box-shadow;
margin-bottom: -$footer-height * 2.5;
.sidenav__link {
padding-right: $gap * 2;
@include media($large-screen) {
padding-right: $gap * 2;
}
}
&__context--portfolio {
.sidenav__link {
padding-right: $gap;
}
}
}

View File

@ -0,0 +1,61 @@
.member-form {
text-align: left;
input[type="checkbox"] + label::before {
margin-left: 0;
}
.input__inline-fields {
text-align: left;
.usa-input__choices label {
font-weight: $font-bold;
}
}
.input__inline-fields {
padding: $gap * 2;
border: 1px solid $color-gray-lighter;
&.checked {
border: 1px solid $color-blue;
}
label {
font-weight: $font-bold;
}
p.usa-input__help {
margin-bottom: 0;
padding-left: 3rem;
}
}
.user-info {
.usa-input {
width: 45rem;
input,
label,
.usa-input__message {
max-width: unset;
}
label .icon-validation {
left: unset;
right: -$gap * 4;
}
&--validation--phoneExt {
width: 18rem;
}
}
}
}
#modal--add-app-mem,
#modal--add-portfolio-manager {
.modal__body {
min-width: 75rem;
}
}

View File

@ -5,13 +5,6 @@
}
margin-left: 2 * $gap;
.line {
box-sizing: border-box;
height: 2px;
width: 100%;
border: 1px solid $color-gray-lightest;
}
}
.portfolio-header {
@ -40,36 +33,6 @@
}
}
&__budget {
font-size: $small-font-size;
align-items: center;
.icon-tooltip {
margin-left: -$gap / 2;
}
button {
margin: 0;
padding: 0;
}
&--dollars {
font-size: $h2-font-size;
font-weight: bold;
}
&--amount {
white-space: nowrap;
}
&--cents {
font-size: 2rem;
margin-top: 0.75rem;
margin-left: -0.7rem;
font-weight: bold;
}
}
.links {
justify-content: center;
font-size: $small-font-size;
@ -109,22 +72,6 @@
}
}
}
.column-left {
width: 12.5rem;
float: left;
}
.column-right {
margin-left: -0.4rem;
}
.unfunded {
color: $color-red;
.icon {
@include icon-color($color-red);
}
}
}
@mixin subheading {
@ -138,6 +85,10 @@
.portfolio-content {
margin: (4 * $gap) $gap 0 $gap;
.panel {
padding-bottom: 2rem;
}
a.add-new-button {
display: inherit;
margin-left: auto;
@ -157,44 +108,6 @@
}
}
input.usa-button.usa-button-primary {
width: 9rem;
height: 4rem;
}
select {
padding-left: 1.2rem;
}
.members-table-ppoc {
select::-ms-expand {
display: none;
color: $color-gray;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: block;
width: 100%;
float: right;
margin: 5px 0px;
padding: 0px 24px;
background-image: none;
-ms-word-break: normal;
word-break: normal;
padding-right: 3rem;
padding-left: 1.2rem;
color: $color-gray;
}
select:hover {
box-shadow: none;
color: $color-gray;
}
}
a.modal-link.icon-link {
float: right;
@ -607,3 +520,28 @@
margin-right: $gap * 3;
}
}
.summary-item {
border-right: 1px solid $color-gray-light;
margin-right: $gap * 3;
padding-right: $gap * 3;
&:last-child {
border-right: none;
margin-right: 0;
padding-right: 0;
}
&__header {
margin: 0;
&-icon {
margin: 0;
padding: 0;
}
}
&__value {
font-size: $lead-font-size;
&--large {
font-size: 3.4rem;
font-weight: $font-bold;
}
}
}

View File

@ -63,4 +63,8 @@
font-size: $small-font-size;
font-weight: $font-bold;
}
&--link {
font-weight: $font-bold;
}
}

View File

@ -12,88 +12,44 @@
flex-direction: row;
align-items: stretch;
justify-content: space-between;
}
.topbar__link {
color: $color-white;
display: inline-flex;
align-items: center;
height: $topbar-height;
padding: 0 ($gap * 2);
&__link {
color: $color-white !important;
display: inline-flex;
align-items: center;
height: $topbar-height;
padding: 0 ($gap * 2);
text-decoration: none;
&-label {
@include h5;
text-decoration: underline;
padding-left: $gap;
text-decoration: none;
&-label {
@include h5;
text-decoration: underline;
padding-left: $gap;
text-decoration: none;
}
&-icon {
margin-left: $gap;
@include icon-color($color-white);
}
&--home {
padding-left: $gap / 2;
}
&--shield {
width: $icon-bar-width;
justify-content: center;
padding: 0;
.topbar__link-icon {
margin: 0;
}
}
&:hover {
background-color: $color-primary-darker;
color: $color-white;
}
}
.topbar__context {
display: flex;
flex-grow: 1;
flex-direction: row;
align-items: stretch;
justify-content: flex-end;
&-icon {
margin-left: $gap;
.topbar__portfolio-menu {
margin-right: auto;
position: relative;
}
@include icon-color($color-white);
}
&--home {
padding-left: $gap / 2;
}
&:hover {
background-color: $color-primary-darker;
color: $color-white;
}
}
&.topbar--public {
background-color: $color-primary;
.topbar__navigation {
justify-content: flex-end;
-ms-flex-pack: justify;
}
.topbar__link {
color: $color-white;
&-icon {
@include icon-style-inverted;
}
&--home {
padding-left: $gap;
}
&:first-child {
margin-right: auto;
}
&:hover {
background-color: $color-primary-darker;
}
}
&__context {
display: flex;
flex-grow: 1;
flex-direction: row;
align-items: stretch;
justify-content: flex-end;
}
}

View File

@ -88,5 +88,9 @@ p {
hr {
border: 0;
border-bottom: 1px solid $color-gray-light;
margin: ($gap * 3) ($site-margins * -4);
margin: ($gap * 3) 0;
&.full-width {
margin: ($gap * 3) ($site-margins * -4);
}
}

View File

@ -17,6 +17,7 @@ $usa-banner-height: 2.8rem;
$sidenav-expanded-width: 25rem;
$sidenav-collapsed-width: 10rem;
$max-panel-width: 80rem;
$home-pg-icon-width: 6rem;
/*
* USWDS Variables

View File

@ -12,6 +12,7 @@
.usa-button,
a {
margin: 0 0 0 $gap;
cursor: pointer;
@include media($medium-screen) {
margin: 0 0 0 ($gap * 2);
@ -46,7 +47,7 @@
background: white;
right: 0;
padding-right: $gap * 4;
border-top: 1px solid $color-gray-light;
border-top: 1px solid $color-gray-lighter;
width: 100%;
z-index: 1;
}

View File

@ -94,4 +94,19 @@
&--primary {
@include icon-color($color-primary);
}
&--home-pg-badge {
@include icon-size(27);
@include icon-color($color-white);
background-color: $color-primary;
height: $home-pg-icon-width;
width: $home-pg-icon-width;
border-radius: 100%;
display: inline-flex;
svg {
margin: auto;
}
}
}

View File

@ -165,6 +165,15 @@
margin-top: 0;
margin-bottom: 0;
}
label {
margin-left: 3rem;
&:before {
position: absolute;
left: -3rem;
}
}
}
select {

View File

@ -1,10 +1,4 @@
@mixin sidenav__header {
padding: $gap ($gap * 2);
font-weight: bold;
}
.sidenav-container {
box-shadow: $box-shadow;
overflow: hidden;
position: relative;
top: $topbar-height + $usa-banner-height;
@ -14,187 +8,11 @@
@extend .sidenav-container;
width: $sidenav-collapsed-width;
}
}
&__fixed {
position: fixed;
}
.sidenav {
width: $sidenav-expanded-width;
@include media($large-screen) {
margin: 0px;
}
&__title {
@include sidenav__header;
font-size: $h3-font-size;
text-transform: uppercase;
width: 50%;
color: $color-gray-dark;
opacity: 0.54;
}
&__toggle {
@include sidenav__header;
font-size: $small-font-size;
line-height: 2.8rem;
float: right;
color: $color-blue-darker;
.toggle-arrows {
vertical-align: middle;
}
}
ul {
&.sidenav__list--padded {
margin-top: 4 * $gap;
margin-bottom: $footer-height;
padding-bottom: $gap;
position: fixed;
overflow-y: scroll;
top: $topbar-height + $usa-banner-height + 4rem;
bottom: 0;
left: 0;
width: $sidenav-expanded-width;
background-color: $color-white;
}
list-style: none;
padding: 0;
li {
margin: 0;
display: block;
}
}
&__divider--small {
display: block;
width: 4 * $gap;
border: 1px solid #d6d7d9;
margin-left: 2 * $gap;
margin-bottom: $gap;
}
&__text {
margin: 2 * $gap;
color: $color-gray;
font-style: italic;
}
&__link {
display: block;
padding: $gap ($gap * 2);
color: $color-black;
text-decoration: underline;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&-icon {
margin-left: -($gap * 0.5);
}
&--disabled {
color: $color-shadow;
pointer-events: none;
}
&--add {
color: $color-blue;
font-size: $small-font-size;
.icon {
@include icon-color($color-blue);
@include icon-size(14);
}
}
&--active {
@include h4;
color: $color-primary;
background-color: $color-aqua-lightest;
box-shadow: inset ($gap / 2) 0 0 0 $color-primary;
.sidenav__link-icon {
@include icon-style-active;
}
position: relative;
&_indicator .icon {
@include icon-color($color-primary);
position: absolute;
right: 0;
}
+ ul {
background-color: $color-primary;
.sidenav__link {
color: $color-white;
background-color: $color-primary;
&:hover {
background-color: $color-blue-darker;
}
&--active {
@include h5;
color: $color-white;
background-color: $color-primary;
box-shadow: none;
}
.icon {
@include icon-color($color-white);
}
}
}
}
+ ul {
li {
.sidenav__link {
@include h5;
padding: $gap * 0.75;
padding-left: 4.5rem;
border: 0;
font-weight: normal;
.sidenav__link-icon {
@include icon-size(12);
flex-shrink: 0;
margin-right: 1.5rem;
margin-left: -3rem;
}
.sidenav__link-label {
padding-left: 0;
}
}
}
}
&:hover {
color: $color-primary;
background-color: $color-aqua-lightest;
.sidenav__link-icon {
@include icon-style-active;
}
}
}
}
.sidenav {
width: $sidenav-expanded-width;
position: fixed;
&--minimized {
@extend .sidenav;
@ -202,4 +20,110 @@
width: $sidenav-collapsed-width;
margin: 0px;
}
@include media($large-screen) {
margin: 0px;
}
&__header {
padding: $gap ($gap * 2);
font-weight: bold;
border-bottom: 1px solid $color-gray-lighter;
&--minimized {
@extend .sidenav__header;
padding: $gap;
width: $sidenav-collapsed-width;
}
}
&__title {
font-size: $h6-font-size;
text-transform: uppercase;
width: 50%;
color: $color-gray-dark;
opacity: 0.54;
white-space: nowrap;
padding: $gap;
display: inline-flex;
align-items: center;
}
&__toggle {
font-size: $small-font-size;
color: $color-blue;
text-decoration: none;
padding: $gap;
display: inline-flex;
align-items: center;
}
&__toggle-arrows {
vertical-align: middle;
@include icon-size(20);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
&__list {
margin-top: 3 * $gap;
margin-bottom: $footer-height;
padding-bottom: ($gap * 2);
position: fixed;
overflow-y: scroll;
top: $topbar-height + $usa-banner-height + 4rem;
bottom: 0;
left: 0;
width: $sidenav-expanded-width;
background-color: $color-white;
list-style: none;
padding: 0;
&--no-header {
top: $topbar-height + $usa-banner-height;
}
}
&__text {
margin: 2 * $gap;
color: $color-gray;
font-style: italic;
}
&__item {
margin: 0;
display: block;
color: $color-black-light !important;
}
&__link {
display: block;
padding: $gap ($gap * 2);
white-space: nowrap;
overflow: hidden;
color: $color-black-light !important;
text-decoration: none;
text-overflow: ellipsis;
&--active {
@include h4;
background-color: $color-aqua-lightest !important;
color: $color-primary-darker !important;
box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker;
position: relative;
}
&:hover {
color: $color-primary !important;
background-color: $color-aqua-lightest;
}
}
}

View File

@ -21,24 +21,8 @@ table.atat-table {
text-align: right;
}
&--align-center {
text-align: center;
}
&--shrink {
width: 1%;
}
&--expand {
width: 100%;
}
&--hide-small {
display: none;
@include media($medium-screen) {
display: table-cell;
}
&--third {
width: 33%;
}
}
}
@ -53,7 +37,6 @@ table.atat-table {
padding: $gap * 2;
border: 1px solid $color-gray-lighter;
display: table-cell;
white-space: nowrap;
vertical-align: top;
&:first-child {
@ -84,28 +67,6 @@ table.atat-table {
@include panel-margin;
&__header {
@include panel-base;
@include panel-theme-default;
border-top: none;
border-bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: $gap * 2;
&__title {
@include h4;
font-size: $lead-font-size;
justify-content: space-between;
flex: 2;
}
}
table {
margin-bottom: 0;
}

View File

@ -23,66 +23,6 @@
}
}
#modal--add-app-mem,
.form-content--app-mem {
text-align: left;
.modal__body {
min-width: 75rem;
}
input[type="checkbox"] + label::before {
margin-left: 0;
}
.input__inline-fields {
text-align: left;
.usa-input__choices label {
font-weight: $font-bold;
}
}
.input__inline-fields {
padding: $gap * 2;
border: 1px solid $color-gray-lighter;
&.checked {
border: 1px solid $color-blue;
}
label {
font-weight: $font-bold;
}
p.usa-input__help {
margin-bottom: 0;
padding-left: 3rem;
}
}
.application-member__user-info {
.usa-input {
width: 45rem;
input,
label,
.usa-input__message {
max-width: unset;
}
label .icon-validation {
left: unset;
right: -$gap * 4;
}
&--validation--phoneExt {
width: 18rem;
}
}
}
}
.environment-roles {
padding: 0 ($gap * 3) ($gap * 3);

View File

@ -1,49 +1,25 @@
.home {
margin: $gap * 3;
.sticky-cta {
margin: -1.6rem -1.6rem 0 -1.6rem;
}
}
.about-cloud {
margin: 4rem auto;
max-width: 900px;
}
&__content {
margin: 4rem;
max-width: 900px;
.your-project {
margin-top: 3rem;
padding: 3rem;
background-color: $color-gray-lightest;
&--descriptions {
.col {
margin-left: $home-pg-icon-width;
padding: ($gap * 2) ($gap * 4);
position: relative;
h2 {
margin-top: 0;
}
.links {
justify-content: flex-start;
.icon-link {
padding: $gap ($gap * 4);
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
&:hover {
background-color: transparent;
color: $color-gray-dark;
.svg * {
fill: $color-gray-dark;
.icon--home-pg-badge {
position: absolute;
left: -$home-pg-icon-width;
top: $gap * 3;
}
}
&.active:hover {
color: $color-blue;
}
}
}
}
@ -112,8 +88,3 @@
}
}
}
#jedi-heirarchy {
max-width: 65rem;
margin-top: $gap * 8;
}

View File

@ -118,27 +118,6 @@
}
}
.reporting-summary-item {
border-right: 1px solid $color-gray-light;
margin-right: $gap * 3;
padding-right: $gap * 3;
&:last-child {
border-right: none;
margin-right: 0;
padding-right: 0;
}
&__header {
margin: 0;
&-icon {
margin: 0;
padding: 0;
}
}
&__value {
font-size: $lead-font-size;
}
}
.reporting-expended-funding {
&__header {
margin: 0;

View File

@ -20,10 +20,7 @@
}
&__header {
.h2,
p {
margin-bottom: $gap * 0.5;
}
margin-bottom: $gap * 6;
}
.col {
@ -39,22 +36,6 @@
margin-top: $gap * 2;
}
.task-order__review {
.h2 {
margin-bottom: $gap * 4;
}
.task-order__number {
text-align: right;
}
.totals-box {
flex-grow: unset;
display: table;
min-width: 350px;
}
}
.card {
padding: ($gap * 4) ($gap * 5) 0;
@ -155,6 +136,10 @@
}
}
}
&__confirmation {
margin-left: $gap * 8;
}
}
.task-order__modal-cancel {
@ -170,3 +155,16 @@
padding-bottom: $gap * 2.5;
}
}
table.clin-summary {
tbody,
thead {
tr {
&:last-child {
td {
border-bottom: none;
}
}
}
}
}

View File

@ -100,7 +100,7 @@
{{ CheckboxInput(form.perms_env_mgmt, classes="input__inline-fields", key=env_mgmt, id=env_mgmt, optional=True) }}
{{ CheckboxInput(form.perms_del_env, classes="input__inline-fields", key=del_env, id=del_env, optional=True) }}
</div>
<hr>
<hr class="full-width">
<div class="environment_roles environment-roles-new">
<h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2>
<p class='usa-input__help subtitle'>
@ -118,7 +118,7 @@
{% endmacro %}
{% macro InfoFields(member_form) %}
<div class="application-member__user-info">
<div class="user-info">
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.email, validation='email', optional=False) }}

View File

@ -1,12 +1,10 @@
{% from "components/alert.html" import Alert %}
{% from "components/icon.html" import Icon %}
{% from "components/label.html" import Label %}
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
{% import "components/member_form.html" as member_form %}
{% import "applications/fragments/member_form_fields.html" as member_fields %}
{% from "components/modal.html" import Modal %}
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% from "components/save_button.html" import SaveButton %}
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{% macro MemberManagementTemplate(
application,
@ -40,7 +38,7 @@
{% call Modal(modal_name, classes="form-content--app-mem") %}
<div class="modal__form--header">
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
<hr>
<hr class="full-width">
</div>
<base-form inline-template>
<form id='{{ modal_name }}' method="POST" action="{{ url_for(action_update, application_id=application.id, application_role_id=member.role_id,) }}">
@ -59,7 +57,7 @@
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
<div class="modal__form--header">
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
<hr>
<hr class="full-width">
</div>
<base-form inline-template :enable-save="true">
<form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('applications.resend_invite', application_id=application.id, application_role_id=member.role_id) }}">
@ -179,8 +177,19 @@
form=new_member_form,
form_action=url_for(action_new, application_id=application.id),
steps=[
member_steps.MemberStepOne(new_member_form),
member_steps.MemberStepTwo(new_member_form, application)
member_form.BasicStep(
title="portfolios.applications.members.form.add_member"|translate,
form=member_fields.InfoFields(new_member_form.user_data),
next_button_text="portfolios.applications.members.form.next_button"|translate,
previous=False,
modal=new_member_modal_name,
),
member_form.SubmitStep(
name=new_member_modal_name,
form=member_fields.PermsFields(form=new_member_form, new=True),
submit_text="portfolios.applications.members.form.add_member"|translate,
modal=new_member_modal_name,
)
],
) }}
{% endif %}

View File

@ -1,50 +0,0 @@
{% from "components/icon.html" import Icon %}
{% import "applications/fragments/member_form_fields.html" as member_fields %}
{% macro MemberFormTemplate(title=None, next_button=None, previous=True) %}
<hr>
{% if title %} <h1>{{ title }}</h1> {% endif %}
{{ caller() }}
<div class='action-group'>
{{ next_button }}
{% if previous %}
<input
type='button'
v-on:click="previous()"
class='action-group__action usa-button usa-button-secondary'
value='{{ "common.previous" | translate }}'>
{% endif %}
<a class='action-group__action' v-on:click="closeModal('{{ new_port_mem }}')">{{ "common.cancel" | translate }}</a>
</div>
{% endmacro %}
{% macro MemberStepOne(member_form) %}
{% set next_button %}
<input
type='button'
v-on:click="next()"
v-bind:disabled="!canSave"
class='action-group__action usa-button'
value='{{ "portfolios.applications.members.form.next_button" | translate }}'>
{% endset %}
{% call MemberFormTemplate(title="portfolios.applications.members.form.add_member"|translate, next_button=next_button, previous=False) %}
{{ member_fields.InfoFields(member_form.user_data) }}
{% endcall %}
{% endmacro %}
{% macro MemberStepTwo(member_form, application) %}
{% set next_button %}
<input
type="submit"
class='action-group__action usa-button'
form="add-app-mem"
v-bind:disabled="!canSave"
value='{{ "portfolios.applications.members.form.add_member" | translate}}'>
{% endset %}
{% call MemberFormTemplate(next_button=next_button) %}
{{ member_fields.PermsFields(form=member_form, new=True) }}
{% endcall %}
{% endmacro %}

View File

@ -30,7 +30,7 @@
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
</div>
</div>
<hr class="panel__break">
<hr>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=True) }}

View File

@ -19,7 +19,7 @@
<p>
{{ 'portfolios.applications.new.step_2_description' | translate }}
</p>
<hr class="panel__break">
<hr>
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit">
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
@ -58,9 +58,9 @@
{{ Icon("plus") }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<span class="action-group-footer">
{% block next_button %}
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}

View File

@ -15,7 +15,7 @@
<p>
{{ ('portfolios.applications.new.step_3_description' | translate) }}
</p>
<hr class="panel__break">
<hr>
{{ MemberManagementTemplate(
application,

View File

@ -13,6 +13,9 @@
{% block application_content %}
{% if show_flash -%}
{% include "fragments/flash.html" %}
{%- endif %}
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
{% if user_can(permissions.EDIT_APPLICATION) %}
@ -59,59 +62,8 @@
environments_obj,
new_env_form) }}
{% if user_can(permissions.DELETE_APPLICATION) %}
{% set env_count = application.environments | length %}
{% if env_count == 1 %}
{% set pluralized_env = "environment" %}
{% else %}
{% set pluralized_env = "environments" %}
{% endif %}
<h3>
{{ "portfolios.applications.delete.subheading" | translate }}
</h3>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ "portfolios.applications.delete.text" | translate({"application_name": application.name}) | safe }}
</div>
<div class="form-col form-col--third">
<div class="usa-input">
<input
id="delete-application"
type="button"
v-on:click="openModal('delete-application')"
class='usa-button--outline button-danger-outline'
value="{{ 'portfolios.applications.delete.button' | translate }}"
>
</div>
</div>
</div>
{% call Modal(name="delete-application") %}
<h1>{{ "portfolios.applications.delete.header" | translate }}</h1>
<hr>
{{
Alert(
title=("components.modal.destructive_title" | translate),
message=("portfolios.applications.delete.alert.message" | translate),
level="warning"
)
}}
{{
DeleteConfirmation(
modal_id="delete_application",
delete_text=('portfolios.applications.delete.button' | translate),
delete_action= url_for('applications.delete', application_id=application.id),
form=application_form
)
}}
{% endcall %}
{% endif %}
<hr>
{% if user_can(permissions.VIEW_APPLICATION_ACTIVITY_LOG) and config.get("USE_AUDIT_LOG", False) %}
<hr>
{% include "fragments/audit_events_log.html" %}
{{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }}
{% endif %}

View File

@ -21,9 +21,7 @@
{% include 'navigation/topbar.html' %}
<div class='global-layout'>
{% if portfolios %}
{% include 'navigation/global_sidenav.html' %}
{% endif %}
{% include 'navigation/global_sidenav.html' %}
<div class='global-panel-container'>
{% block sidenav %}{% endblock %}

View File

@ -9,11 +9,15 @@
"text": "changes pending",
"color": "default",
},
"ppoc": {"text": "primary point of contact"}
} %}
{% if type -%}
{% if type in label_info.keys() -%}
<span class='label label--{{ label_info[type]["color"] }} {{ classes }}'>
{{ Icon(label_info[type]["icon"]) }} {{ label_info[type]["text"] }}
{% if label_info[type]["icon"] %}
{{ Icon(label_info[type]["icon"]) }}
{% endif %}
{{ label_info[type]["text"] }}
</span>
{%- endif %}
{%- endmacro %}

View File

@ -0,0 +1,65 @@
<!-- Layout macro -->
{% macro MemberForm(title=None, next_button=None, previous=True, modal=modal) %}
<div class="member-form">
<hr class="full-width">
{% if title %} <h2>{{ title }}</h2> {% endif %}
{{ caller() }}
</div>
<div class='action-group'>
{{ next_button }}
{% if previous %}
<input
type='button'
v-on:click="previous()"
class='action-group__action usa-button usa-button-secondary'
value='{{ "common.previous" | translate }}'>
{% endif %}
<a class='action-group__action' v-on:click="closeModal('{{ modal }}')">{{ "common.cancel" | translate }}</a>
</div>
{% endmacro %}
<!-- Step macros to use with MultiStepModalForm -->
{% macro BasicStep(
title=None,
form=form,
next_button_text=next_button_text,
previous=True,
modal=modal
) %}
{% set next_button %}
<input
type='button'
v-on:click="next()"
v-bind:disabled="!canSave"
class='action-group__action usa-button'
value='{{ next_button_text }}'>
{% endset %}
{% call MemberForm(title=title, next_button=next_button, previous=previous, modal=modal) %}
{{ form }}
{% endcall %}
{% endmacro %}
{% macro SubmitStep(
name=name,
title=None,
form=form,
submit_text=submit_text,
previous=True,
modal=modal
) %}
{% set next_button %}
<input
type="submit"
class='action-group__action usa-button'
form="{{ name }}"
v-bind:disabled="!canSave"
value='{{ submit_text }}'>
{% endset %}
{% call MemberForm(title=title, next_button=next_button, previous=previous, modal=modal) %}
{{ form }}
{% endcall %}
{% endmacro %}

View File

@ -1,35 +1,9 @@
{% from "components/icon.html" import Icon %}
{% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%}
<li>
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
{% if icon %}
{{ Icon(icon, classes="sidenav__link-icon") }}
{% endif %}
<span class="sidenav__link-label">
{{label}}
</span>
{% if active %}
<span class="sidenav__link-active_indicator">
{{ Icon("caret_right") }}
{% macro SidenavItem(label, href, active=False) -%}
<li class="sidenav__item">
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
<span class="sidenav__link-label">
{{label}}
</span>
{% endif %}
</a>
{% if subnav and active %}
<ul>
{% for item in subnav %}
<li>
<a class="sidenav__link {% if item["active"] %}sidenav__link--active{% endif %}" href="{{item["href"]}}" title="{{item["label"]}}">
{% if "icon" in item %}
{{ Icon(item["icon"], classes="sidenav__link-icon") }}
{% endif %}
<span class="sidenav__link-label">{{item["label"]}}</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</a>
</li>
{%- endmacro %}

View File

@ -3,7 +3,7 @@
{% macro Tooltip(message,title='Help', classes="") %}
<button type="button" tabindex="0" class="icon-tooltip {{classes}}" v-tooltip.top="{content: '{{message}}', container: false}">
{{ Icon('help') }}<span>{{ title }}</span>
{{ Icon('question') }}<span>{{ title }}</span>
</button>
{%- endmacro %}

View File

@ -1,14 +1,8 @@
{% from "components/icon.html" import Icon %}
<footer class='app-footer'>
<div class='app-footer__info'>
<a href="#" class='icon-link app-footer__info__link' target='_blank' rel='noopener noreferrer'>
{{ Icon('help', classes='icon--footer') }}
<span>{{ "footer.jedi_help_link_text" | translate }}</span>
</a>
</div>
{% if g.last_login %}
<div class="">
<div class="app-footer__login">
{{ "footer.login" | translate }} <local-datetime timestamp='{{ g.last_login }}' format='MMM D YYYY H:mm Z'></local-datetime>
</div>
{% endif %}

View File

@ -6,31 +6,26 @@
{% block content %}
<div class='global-layout'>
<div class='global-navigation sidenav'>
<ul>
{{ SidenavItem("JEDI Cloud Help",
href = url_for("atst.helpdocs"),
active = not doc,
icon='help'
)}}
{% for doc_item in docs %}
{% set active = doc and doc == doc_item %}
{{ SidenavItem(doc_item | title,
href = url_for("atst.helpdocs", doc=doc_item),
active = active,
subnav = subnav or None
)}}
{% endfor %}
</ul>
<div class='global-navigation'>
<div class="sidenav-container">
<div class="sidenav">
<ul class="sidenav__list sidenav__list--no-header">
{{ SidenavItem("JEDI Cloud Help",
href = url_for("atst.helpdocs"),
active = not doc,
)}}
{% for doc_item in docs %}
{% set active = doc and doc == doc_item %}
{{ SidenavItem(doc_item | title,
href = url_for("atst.helpdocs", doc=doc_item),
active = active,
)}}
{% endfor %}
</ul>
</div>
</div>
</div>
<div class='global-panel-container'>
<div class='panel'>
<div class='panel__heading panel__heading--divider'>
<h1>
@ -42,16 +37,12 @@
{% endif %}
</h1>
</div>
<div class='panel__content'>
{% block doc_content %}
<p>Welcome to the JEDI Cloud help documentation.</p>
{% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,8 +1,7 @@
{% extends "base.html" %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% from "components/icon.html" import Icon %}
{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% block content %}
@ -13,88 +12,56 @@
{% set sticky_header = "home.get_started" | translate %}
{% endif %}
{% call StickyCTA(sticky_header) %}
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button-primary">
{{ "home.add_portfolio_button_text" | translate }}
</a>
{% endcall %}
<div class="about-cloud">
<div class="home__content">
{% include "fragments/flash.html" %}
<h1>{{ "home.head" | translate }}</h1>
<h3>Set up a Portfolio</h3>
<h4>New Portfolios will be visible in the left side bar of this page. </h4>
<p>All TOs associated to a specific Application or set of related Applications will be entered at the Portfolio level. Funding is applied and managed at the Portfolio level as well.</p>
<hr>
{{ SemiCollapsibleText(first_half=("home.about_cloud.part1"|translate), second_half=("home.about_cloud.part2"|translate)) }}
<div class="your-project">
<h2 class="h3">{{ "home.your_project" | translate }}</h2>
<p>{{ "home.your_project_descrip" | translate }}</p>
<hr>
{% macro Link(icon, text, section, default=False) %}
{% if default %}
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}" || selectedSection === null}' v-on:click="toggleSection('{{ section }}')">
{% else %}
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}"}' v-on:click="toggleSection('{{ section }}')">
{% endif %}
<div class="col">
<div class='icon-link--icon'>{{ Icon(icon) }}</div>
<div class='icon-link--name'>{{ text }}</div>
</div>
<div class="home__content--descriptions">
<div class="row">
<div class="col col--half col--pad">
{{ Icon('funding', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.funding" | translate }}</h4>
<p>
{{ "home.funding_descrip" | translate }}
</p>
</div>
{% endmacro %}
<toggler inline-template v-bind:initial-selected-section="'funding'">
<div>
<div class="portfolio-header">
<div class="links row">
{{ Link(
icon='funding',
section='funding',
text='navigation.portfolio_navigation.breadcrumbs.funding' | translate,
default=True
) }}
{{ Link(
icon='applications',
section='applications',
text='navigation.portfolio_navigation.breadcrumbs.applications' | translate,
) }}
{{ Link(
icon='chart-pie',
section='reports',
text='navigation.portfolio_navigation.breadcrumbs.reports' | translate,
) }}
{{ Link(
icon='cog',
section='admin',
text='navigation.portfolio_navigation.breadcrumbs.admin' | translate,
) }}
</div>
</div>
{% macro Description(section, default=False) %}
{% if default %}
<p v-show="selectedSection === '{{ section }}' || selectedSection === null">
{% else %}
<p v-show="selectedSection === '{{ section }}'">
{% endif %}
<strong>
{{ "navigation.portfolio_navigation.breadcrumbs.%s" | format(section) | translate }}
</strong>
{{ "home.%s_descrip" | format(section) | translate }}
</p>
{% endmacro %}
<div class="project-section-descriptions">
{{ Description('funding', default=True) }}
{{ Description('applications') }}
{{ Description('reports') }}
{{ Description('admin') }}
</div>
<div class="col col--half col--pad">
{{ Icon('chart-pie', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.reports" | translate }}</h4>
<p>
{{ "home.reports_descrip" | translate }}
</p>
</div>
</toggler>
</div>
<div class="row">
<div class="col col--half col--pad">
{{ Icon('applications', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.applications" | translate }}</h4>
<p>
{{ "home.applications_descrip" | translate }}
</p>
</div>
<div class="col col--half col--pad">
{{ Icon('cog', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.admin" | translate }}</h4>
<p>
{{ "home.admin_descrip" | translate }}
</p>
</div>
</div>
</div>
<div class="empty-state">
<div class="empty-state__footer">
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button usa-button-primary">
{{ "home.add_portfolio_button_text" | translate }}
</a>
</div>
</div>
<img id='jedi-heirarchy' src="{{ url_for("static", filename="img/JEDIhierarchyDiagram.png")}}" alt="JEDI heirarchy diagram">
</div>
</main>

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