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$", "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null "lines": null
}, },
"generated_at": "2019-12-06T21:22:07Z", "generated_at": "2020-01-09T11:35:03Z",
"plugins_used": [ "plugins_used": [
{ {
"base64_limit": 4.5, "base64_limit": 4.5,
@ -98,7 +98,7 @@
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 29, "line_number": 30,
"type": "Secret Keyword" "type": "Secret Keyword"
} }
], ],
@ -111,15 +111,6 @@
"type": "Secret Keyword" "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": [ "ssl/client-certs/atat.mil.key": [
{ {
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b", "hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
@ -170,7 +161,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 656, "line_number": 665,
"type": "Hex High Entropy String" "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/config/ ./config/
COPY --from=builder /install/templates/ ./templates/ COPY --from=builder /install/templates/ ./templates/
COPY --from=builder /install/translations.yaml . COPY --from=builder /install/translations.yaml .
COPY --from=builder /install/script/seed_roles.py ./script/seed_roles.py COPY --from=builder /install/script/ ./script/
COPY --from=builder /install/script/sync-crls ./script/sync-crls
COPY --from=builder /install/static/ ./static/ COPY --from=builder /install/static/ ./static/
COPY --from=builder /install/fixtures/ ./fixtures COPY --from=builder /install/fixtures/ ./fixtures
COPY --from=builder /install/uwsgi.ini . COPY --from=builder /install/uwsgi.ini .

View File

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

225
Pipfile.lock generated
View File

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

View File

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

View File

@ -6,6 +6,7 @@ from atst.models.user import User
from atst.models.application import Application from atst.models.application import Application
from atst.models.environment import Environment from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole from atst.models.environment_role import EnvironmentRole
from .policy import AzurePolicyManager
class GeneralCSPException(Exception): class GeneralCSPException(Exception):
@ -401,6 +402,7 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
class AzureSDKProvider(object): class AzureSDKProvider(object):
def __init__(self): def __init__(self):
from azure.mgmt import subscription, authorization, managementgroups from azure.mgmt import subscription, authorization, managementgroups
from azure.mgmt.resource import policy
import azure.graphrbac as graphrbac import azure.graphrbac as graphrbac
import azure.common.credentials as credentials import azure.common.credentials as credentials
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
@ -410,6 +412,7 @@ class AzureSDKProvider(object):
self.managementgroups = managementgroups self.managementgroups = managementgroups
self.graphrbac = graphrbac self.graphrbac = graphrbac
self.credentials = credentials self.credentials = credentials
self.policy = policy
# may change to a JEDI cloud # may change to a JEDI cloud
self.cloud = AZURE_PUBLIC_CLOUD self.cloud = AZURE_PUBLIC_CLOUD
@ -427,6 +430,8 @@ class AzureCloudProvider(CloudProviderInterface):
else: else:
self.sdk = azure_sdk_provider self.sdk = azure_sdk_provider
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
def create_environment( def create_environment(
self, auth_credentials: Dict, user: User, environment: 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 # but we just don't have a valid ID
pass 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): def _get_management_service_principal(self):
# we really should be using graph.microsoft.com, but i'm getting # we really should be using graph.microsoft.com, but i'm getting
# "expired token" errors for that # "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"] 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( return sorted(
[ [
cls._get_application_monthly_totals(application) cls._get_application_monthly_totals(portfolio, fixture_app)
for application in applications for fixture_app in fixture_apps
if fixture_app["name"]
in [application.name for application in portfolio.applications]
], ],
key=lambda app: app["name"], key=lambda app: app["name"],
) )
return []
@classmethod @classmethod
def _get_environment_monthly_totals(cls, environment): def _get_environment_monthly_totals(cls, environment):
@ -64,7 +72,7 @@ class MockReportingProvider:
} }
@classmethod @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 returns a dictionary that represents spending totals for an application
and its environments e.g. and its environments e.g.
@ -83,19 +91,28 @@ class MockReportingProvider:
] ]
} }
""" """
environments = sorted( application_envs = [
[ env
for env in portfolio.all_environments
if env.application.name == fixture_app["name"]
]
environments = [
cls._get_environment_monthly_totals(env) cls._get_environment_monthly_totals(env)
for env in application["environments"] for env in fixture_app["environments"]
], if env["name"] in [e.name for e in application_envs]
key=lambda env: env["name"], ]
)
for env in application_envs:
if env.name not in [env["name"] for env in environments]:
environments.append({"name": env.name})
return { return {
"name": application["name"], "name": fixture_app["name"],
"this_month": sum(env["this_month"] for env in environments), "this_month": sum(env.get("this_month", 0) for env in environments),
"last_month": sum(env["last_month"] for env in environments), "last_month": sum(env.get("last_month", 0) for env in environments),
"total": sum(env["total"] for env in environments), "total": sum(env.get("total", 0) for env in environments),
"environments": environments, "environments": sorted(environments, key=lambda env: env["name"]),
} }
@classmethod @classmethod

View File

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

View File

@ -75,10 +75,10 @@ class Portfolios(object):
permission_sets = PortfolioRoles._permission_sets_for_names( permission_sets = PortfolioRoles._permission_sets_for_names(
member_data.get("permission_sets", []) 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( 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) PortfoliosQuery.add_and_commit(role)
@ -107,4 +107,7 @@ class Portfolios(object):
if "name" in new_data: if "name" in new_data:
portfolio.name = new_data["name"] portfolio.name = new_data["name"]
if "description" in new_data:
portfolio.description = new_data["description"]
PortfoliosQuery.add_and_commit(portfolio) PortfoliosQuery.add_and_commit(portfolio)

View File

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

View File

@ -117,12 +117,3 @@ class Users(object):
user.last_session_id = session_id user.last_session_id = session_id
db.session.add(user) db.session.add(user)
db.session.commit() 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"),) description = TextAreaField(translate("forms.portfolio.description.label"),)
class PortfolioCreationForm(PortfolioForm):
defense_component = SelectMultipleField( defense_component = SelectMultipleField(
choices=SERVICE_BRANCHES, choices=SERVICE_BRANCHES,
widget=ListWidget(prefix_label=False), widget=ListWidget(prefix_label=False),

View File

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

View File

@ -151,3 +151,6 @@ class SignatureForm(BaseForm):
translate("task_orders.sign.digital_signature_description"), translate("task_orders.sign.digital_signature_description"),
validators=[Required()], 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 sqlalchemy.orm import relationship, synonym
from atst.models.base import Base from atst.models.base import Base
@ -34,6 +34,11 @@ class Application(
), ),
) )
members = synonym("roles") members = synonym("roles")
__table_args__ = (
UniqueConstraint(
"name", "portfolio_id", name="applications_name_portfolio_id_key"
),
)
@property @property
def users(self): 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.orm import relationship
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from enum import Enum from enum import Enum
@ -38,6 +38,12 @@ class Environment(
primaryjoin="and_(EnvironmentRole.environment_id == Environment.id, EnvironmentRole.deleted == False)", 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): class ProvisioningStatus(Enum):
PENDING = "pending" PENDING = "pending"
COMPLETED = "completed" COMPLETED = "completed"

View File

@ -1,5 +1,6 @@
from sqlalchemy import Column, String from sqlalchemy import Column, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.types import ARRAY
from itertools import chain from itertools import chain
from atst.models.base import Base from atst.models.base import Base
@ -20,7 +21,7 @@ class Portfolio(
name = Column(String, nullable=False) name = Column(String, nullable=False)
description = Column(String) description = Column(String)
defense_component = Column( defense_component = Column(
String, nullable=False ARRAY(String), nullable=False
) # Department of Defense Component ) # Department of Defense Component
applications = relationship( 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 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): class Status(Enum):
ACTIVE = "active" ACTIVE = "active"
DISABLED = "disabled" DISABLED = "disabled"
@ -90,23 +79,23 @@ class PortfolioRole(
@property @property
def display_status(self): def display_status(self):
if self.status == Status.ACTIVE: if self.status == Status.ACTIVE:
return MEMBER_STATUSES["active"] return "active"
elif self.status == Status.DISABLED: elif self.status == Status.DISABLED:
return MEMBER_STATUSES["disabled"] return "disabled"
elif self.latest_invitation: elif self.latest_invitation:
if self.latest_invitation.is_revoked: if self.latest_invitation.is_revoked:
return MEMBER_STATUSES["revoked"] return "invite_revoked"
elif self.latest_invitation.is_rejected_wrong_user: elif self.latest_invitation.is_rejected_wrong_user:
return MEMBER_STATUSES["error"] return "invite_error"
elif ( elif (
self.latest_invitation.is_rejected_expired self.latest_invitation.is_rejected_expired
or self.latest_invitation.is_expired or self.latest_invitation.is_expired
): ):
return MEMBER_STATUSES["expired"] return "invite_expired"
else: else:
return MEMBER_STATUSES["pending"] return "invite_pending"
else: else:
return MEMBER_STATUSES["unknown"] return "unknown"
def has_permission_set(self, perm_set_name): def has_permission_set(self, perm_set_name):
return first_or_none( 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.types as types
import atst.models.mixins as mixins import atst.models.mixins as mixins
from atst.models.attachment import Attachment from atst.models.attachment import Attachment
from atst.utils.clock import Clock from pendulum import today
class Status(Enum): class Status(Enum):
@ -83,26 +83,10 @@ class TaskOrder(Base, mixins.TimestampsMixin):
def is_active(self): def is_active(self):
return self.status == Status.ACTIVE return self.status == Status.ACTIVE
@property
def is_upcoming(self):
return self.status == Status.UPCOMING
@property @property
def is_expired(self): def is_expired(self):
return self.status == Status.EXPIRED 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 @property
def clins_are_completed(self): def clins_are_completed(self):
return all([len(self.clins), (clin.is_completed for clin in self.clins)]) return all([len(self.clins), (clin.is_completed for clin in self.clins)])
@ -117,17 +101,17 @@ class TaskOrder(Base, mixins.TimestampsMixin):
@property @property
def status(self): def status(self):
today = Clock.today() todays_date = today(tz="UTC").date()
if not self.is_completed and not self.is_signed: if not self.is_completed and not self.is_signed:
return Status.DRAFT return Status.DRAFT
elif self.is_completed and not self.is_signed: elif self.is_completed and not self.is_signed:
return Status.UNSIGNED return Status.UNSIGNED
elif today < self.start_date: elif todays_date < self.start_date:
return Status.UPCOMING return Status.UPCOMING
elif today >= self.end_date: elif todays_date > self.end_date:
return Status.EXPIRED return Status.EXPIRED
elif self.start_date <= today < self.end_date: elif self.start_date <= todays_date <= self.end_date:
return Status.ACTIVE return Status.ACTIVE
@property @property
@ -141,39 +125,25 @@ class TaskOrder(Base, mixins.TimestampsMixin):
@property @property
def days_to_expiration(self): def days_to_expiration(self):
if self.end_date: if self.end_date:
return (self.end_date - Clock.today()).days return (self.end_date - today(tz="UTC").date()).days
@property @property
def total_obligated_funds(self): def total_obligated_funds(self):
total = 0 return sum(
for clin in self.clins: (clin.obligated_amount for clin in self.clins if clin.obligated_amount)
if clin.obligated_amount is not None: )
total += clin.obligated_amount
return total
@property @property
def total_contract_amount(self): def total_contract_amount(self):
total = 0 return sum((clin.total_amount for clin in self.clins if clin.total_amount))
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
@property @property
def invoiced_funds(self): def invoiced_funds(self):
# TODO: implement this using reporting data from the CSP # TODO: implement this using reporting data from the CSP
if self.is_active:
return self.total_obligated_funds * Decimal(0.75) return self.total_obligated_funds * Decimal(0.75)
else:
return 0
@property @property
def display_status(self): 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.orm import relationship
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.event import listen from sqlalchemy.event import listen
@ -67,8 +67,6 @@ class User(
last_login = Column(TIMESTAMP(timezone=True), nullable=True) last_login = Column(TIMESTAMP(timezone=True), nullable=True)
last_session_id = Column(UUID(as_uuid=True), nullable=True) last_session_id = Column(UUID(as_uuid=True), nullable=True)
provisional = Column(Boolean)
cloud_id = Column(String) cloud_id = Column(String)
REQUIRED_FIELDS = [ REQUIRED_FIELDS = [

View File

@ -19,6 +19,7 @@ from werkzeug.exceptions import NotFound
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.authnid import AuthenticationContext from atst.domain.authnid import AuthenticationContext
from atst.domain.auth import logout as _logout from atst.domain.auth import logout as _logout
from atst.domain.exceptions import UnauthenticatedError
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
@ -64,11 +65,15 @@ def catch_all(path):
raise NotFound() raise NotFound()
def _client_s_dn():
return request.environ.get("HTTP_X_SSL_CLIENT_S_DN")
def _make_authentication_context(): def _make_authentication_context():
return AuthenticationContext( return AuthenticationContext(
crl_cache=app.crl_cache, crl_cache=app.crl_cache,
auth_status=request.environ.get("HTTP_X_SSL_CLIENT_VERIFY"), 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"), cert=request.environ.get("HTTP_X_SSL_CLIENT_CERT"),
) )
@ -89,19 +94,24 @@ def current_user_setup(user):
session["user_id"] = user.id session["user_id"] = user.id
session["last_login"] = user.last_login session["last_login"] = user.last_login
app.session_limiter.on_login(user) app.session_limiter.on_login(user)
app.logger.info(f"authentication succeeded for user with EDIPI {user.dod_id}")
Users.update_last_login(user) Users.update_last_login(user)
@bp.route("/login-redirect") @bp.route("/login-redirect")
def login_redirect(): def login_redirect():
try:
auth_context = _make_authentication_context() auth_context = _make_authentication_context()
auth_context.authenticate() auth_context.authenticate()
user = auth_context.get_user() user = auth_context.get_user()
if user.provisional:
Users.finalize(user)
current_user_setup(user) current_user_setup(user)
except UnauthenticatedError as err:
app.logger.info(
f"authentication failed for subject distinguished name {_client_s_dn()}"
)
raise err
return redirect(redirect_after_login_url()) 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 .blueprint import applications_bp
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.portfolios import Portfolios
from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.models.permissions import Permissions from atst.models.permissions import Permissions
@ -12,6 +11,7 @@ from atst.routes.applications.settings import (
get_new_member_form, get_new_member_form,
handle_create_member, handle_create_member,
handle_update_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( form = get_new_application_form(
{**http_request.form}, NameAndDescriptionForm, application_id {**http_request.form}, NameAndDescriptionForm, application_id
) )
application = handle_update_application(form, application_id, portfolio_id)
if form.validate(): if application:
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)
return redirect( return redirect(
url_for( url_for(
"applications.update_new_application_step_2", "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 .blueprint import applications_bp
from atst.domain.exceptions import AlreadyExistsError 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.common import Paginator
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.invitations import ApplicationInvitations 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_member import NewForm as NewMemberForm, UpdateMemberForm
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS 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 # 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") @applications_bp.route("/applications/<application_id>/settings")
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form") @user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
def settings(application_id): def settings(application_id):
application = Applications.get(application_id) application = Applications.get(application_id)
return render_settings_page( return render_settings_page(application=application,)
application=application,
active_toggler=http_request.args.get("active_toggler"),
active_toggler_section=http_request.args.get("active_toggler_section"),
)
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"]) @applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
@ -264,31 +314,21 @@ def update_environment(environment_id):
application = environment.application application = environment.application
env_form = EditEnvironmentForm(obj=environment, formdata=http_request.form) 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(): if updated_environment:
Environments.update(environment=environment, name=env_form.name.data)
flash("application_environments_updated")
return redirect( return redirect(
url_for( url_for(
"applications.settings", "applications.settings",
application_id=application.id, application_id=application.id,
fragment="application-environments", fragment="application-environments",
_anchor="application-environments", _anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
) )
) )
else: else:
return ( return (render_settings_page(application=application, show_flash=True), 400)
render_settings_page(
application=application,
active_toggler=environment.id,
active_toggler_section="edit",
),
400,
)
@applications_bp.route( @applications_bp.route(
@ -298,14 +338,9 @@ def update_environment(environment_id):
def new_environment(application_id): def new_environment(application_id):
application = Applications.get(application_id) application = Applications.get(application_id)
env_form = EditEnvironmentForm(formdata=http_request.form) env_form = EditEnvironmentForm(formdata=http_request.form)
environment = handle_update_environment(form=env_form, application=application)
if env_form.validate(): if environment:
Environments.create(
g.current_user, application=application, name=env_form.name.data
)
flash("environment_added", environment_name=env_form.data["name"])
return redirect( return redirect(
url_for( url_for(
"applications.settings", "applications.settings",
@ -315,7 +350,7 @@ def new_environment(application_id):
) )
) )
else: 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"]) @applications_bp.route("/applications/<application_id>/edit", methods=["POST"])
@ -323,10 +358,9 @@ def new_environment(application_id):
def update(application_id): def update(application_id):
application = Applications.get(application_id) application = Applications.get(application_id)
form = NameAndDescriptionForm(http_request.form) form = NameAndDescriptionForm(http_request.form)
if form.validate(): updated_application = handle_update_application(form, application_id)
application_data = form.data
Applications.update(application, application_data)
if updated_application:
return redirect( return redirect(
url_for( url_for(
"applications.portfolio_applications", "applications.portfolio_applications",
@ -334,21 +368,9 @@ def update(application_id):
) )
) )
else: else:
return render_settings_page(application=application, application_form=form) return (
render_settings_page(application=application, show_flash=True),
400,
@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
)
) )

View File

@ -17,63 +17,51 @@ from atst.utils.flash import formatted_flash as flash
from atst.domain.exceptions import UnauthorizedError from atst.domain.exceptions import UnauthorizedError
def permission_str(member, edit_perm_set, view_perm_set): def filter_perm_sets_data(member):
if member.has_permission_set(edit_perm_set): perm_sets_data = {
return edit_perm_set "perms_portfolio_mgmt": bool(
else: member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
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,
), ),
"perms_funding": permission_str( "perms_app_mgmt": bool(
member, member.has_permission_set(
PermissionSets.EDIT_PORTFOLIO_FUNDING, PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
PermissionSets.VIEW_PORTFOLIO_FUNDING, )
), ),
"perms_reporting": permission_str( "perms_funding": bool(
member, member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING)
PermissionSets.EDIT_PORTFOLIO_REPORTS,
PermissionSets.VIEW_PORTFOLIO_REPORTS,
), ),
"perms_portfolio_mgmt": permission_str( "perms_reporting": bool(
member, member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
PermissionSets.EDIT_PORTFOLIO_ADMIN,
PermissionSets.VIEW_PORTFOLIO_ADMIN,
), ),
} }
return perm_sets_data
def get_members_data(portfolio):
members = sorted( def filter_members_data(members_list, portfolio):
[serialize_member_form_data(member) for member in portfolio.members], members_data = []
key=lambda member: member["member_name"], 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
}
) )
for member in members:
if member["member_id"] == portfolio.owner_role.id: return sorted(members_data, key=lambda member: member["user_name"])
ppoc = member
members.remove(member)
members.insert(0, ppoc)
return members
def render_admin_page(portfolio, form=None): def render_admin_page(portfolio, form=None):
pagination_opts = Paginator.get_pagination_opts(http_request) pagination_opts = Paginator.get_pagination_opts(http_request)
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts) audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
members_data = get_members_data(portfolio) portfolio_form = PortfolioForm(obj=portfolio)
portfolio_form = PortfolioForm(data={"name": portfolio.name}) member_list = portfolio.members
member_perms_form = member_forms.MembersPermissionsForm(
data={"members_permissions": members_data}
)
assign_ppoc_form = member_forms.AssignPPOCForm() assign_ppoc_form = member_forms.AssignPPOCForm()
for pf_role in portfolio.roles: for pf_role in portfolio.roles:
if pf_role.user != portfolio.owner and pf_role.is_active: if pf_role.user != portfolio.owner and pf_role.is_active:
assign_ppoc_form.role_id.choices += [(pf_role.id, pf_role.full_name)] 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", "portfolios/admin.html",
form=form, form=form,
portfolio_form=portfolio_form, portfolio_form=portfolio_form,
member_perms_form=member_perms_form, members=filter_members_data(member_list, portfolio),
member_form=member_forms.NewForm(), new_manager_form=member_forms.NewForm(),
assign_ppoc_form=assign_ppoc_form, assign_ppoc_form=assign_ppoc_form,
portfolio=portfolio, portfolio=portfolio,
audit_events=audit_events, audit_events=audit_events,
user=g.current_user, user=g.current_user,
ppoc_id=members_data[0].get("member_id"),
current_member_id=current_member_id, current_member_id=current_member_id,
applications_count=len(portfolio.applications), applications_count=len(portfolio.applications),
) )
@ -106,34 +93,6 @@ def admin(portfolio_id):
return render_admin_page(portfolio) 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"]) @portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
@user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc") @user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
def update_ppoc(portfolio_id): def update_ppoc(portfolio_id):

View File

@ -56,13 +56,3 @@ def reports(portfolio_id):
monthly_spending=Reports.monthly_spending(portfolio), monthly_spending=Reports.monthly_spending(portfolio),
retrieved=datetime.now(), # mocked datetime of reporting data retrival 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(): if form.validate():
try: try:
invite = Portfolios.invite(portfolio, g.current_user, form.update_data) invite = Portfolios.invite(portfolio, g.current_user, form.data)
send_portfolio_invitation( send_portfolio_invitation(
invitee_email=invite.email, invitee_email=invite.email,
inviter_name=g.current_user.full_name, 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 from atst.models import Permissions
@task_orders_bp.route("/task_orders/<task_order_id>/review") @task_orders_bp.route("/task_orders/<task_order_id>")
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="review task order details") @user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order details")
def review_task_order(task_order_id): def view_task_order(task_order_id):
task_order = TaskOrders.get(task_order_id) task_order = TaskOrders.get(task_order_id)
if task_order.is_draft: if task_order.is_draft:
return redirect(url_for("task_orders.edit", task_order_id=task_order.id)) return redirect(url_for("task_orders.edit", task_order_id=task_order.id))
else: else:
signature_form = SignatureForm() signature_form = SignatureForm()
return render_template( return render_template(
"task_orders/review.html", "task_orders/view.html",
task_order=task_order, task_order=task_order,
signature_form=signature_form, signature_form=signature_form,
) )

View File

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

View File

@ -2,16 +2,22 @@ import datetime
import json import json
import logging import logging
from flask import g, request, has_request_context from flask import g, request, has_request_context, session
class RequestContextFilter(logging.Filter): class RequestContextFilter(logging.Filter):
def filter(self, record): def filter(self, record):
if has_request_context(): if has_request_context():
if getattr(g, "current_user", None): if getattr(g, "current_user", None):
record.user_id = str(g.current_user.id)
record.dod_edipi = g.current_user.dod_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"): if request.environ.get("HTTP_X_REQUEST_ID"):
record.request_id = 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")), ("request_id", lambda r: r.__dict__.get("request_id")),
("user_id", lambda r: r.__dict__.get("user_id")), ("user_id", lambda r: r.__dict__.get("user_id")),
("dod_edipi", lambda r: r.__dict__.get("dod_edipi")), ("dod_edipi", lambda r: r.__dict__.get("dod_edipi")),
("logged_in", lambda r: r.__dict__.get("logged_in")),
("severity", lambda r: r.levelname), ("severity", lambda r: r.levelname),
("tags", lambda r: r.__dict__.get("tags")), ("tags", lambda r: r.__dict__.get("tags")),
("audit_event", lambda r: r.__dict__.get("audit_event")), ("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: for field, func in self._DEFAULT_RECORD_FIELDS:
result = func(record) result = func(record)
if result: if result is not None:
message_dict[field] = result message_dict[field] = result
if record.args: if record.args:

View File

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

View File

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

View File

@ -45,7 +45,7 @@ data:
include /etc/nginx/snippets/ssl.conf; include /etc/nginx/snippets/ssl.conf;
location /login-redirect { location /login-redirect {
return 301 https://auth-azure.atat.code.mil$request_uri; return 301 https://${AUTH_DOMAIN}$request_uri;
} }
location /login-dev { location /login-dev {
try_files $uri @appbasicauth; try_files $uri @appbasicauth;
@ -82,7 +82,7 @@ data:
include /etc/nginx/snippets/ssl.conf; include /etc/nginx/snippets/ssl.conf;
location / { location / {
return 301 https://azure.atat.code.mil$request_uri; return 301 https://${MAIN_DOMAIN}$request_uri;
} }
location /login-redirect { location /login-redirect {
try_files $uri @app; 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/ - ../../azure/
resources: resources:
- namespace.yml - namespace.yml
- reset-cron-job.yml
patchesStrategicMerge: patchesStrategicMerge:
- replica_count.yml - replica_count.yml
- ports.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 { emitFieldChange } from '../lib/emitters'
import escape from '../lib/escape'
import optionsinput from './options_input' import optionsinput from './options_input'
import textinput from './text_input' import textinput from './text_input'
import clindollaramount from './clin_dollar_amount' import clindollaramount from './clin_dollar_amount'
@ -99,7 +100,7 @@ export default {
computed: { computed: {
clinTitle: function() { clinTitle: function() {
if (!!this.clinNumber) { if (!!this.clinNumber) {
return `CLIN ${this.clinNumber}` return escape(`CLIN ${this.clinNumber}`)
} else { } else {
return `CLIN` 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 #!/bin/bash
set -e
# script/cibuild: Run CI related checks and tests # script/cibuild: Run CI related checks and tests

View File

@ -26,28 +26,16 @@ fi
## Main ## Main
if [ "${RUN_PYTHON_TESTS}" = "true" ]; then if [ "${RUN_PYTHON_TESTS}" = "true" ]; then
python_test_status=0
set +e
output_divider "Lint Python files" output_divider "Lint Python files"
run_python_lint "${PYTHON_FILES}" run_python_lint "${PYTHON_FILES}"
((python_test_status+=$?))
output_divider "Perform static analysis on Python files" output_divider "Perform static analysis on Python files"
run_python_static_analysis "${PYTHON_FILES}" run_python_static_analysis "${PYTHON_FILES}"
((python_test_status+=$?))
output_divider "Perform type checking on Python files" output_divider "Perform type checking on Python files"
run_python_typecheck run_python_typecheck
((python_test_status+=$?))
output_divider "Run Python unit test suite" output_divider "Run Python unit test suite"
run_python_unit_tests "${PYTHON_FILES}" 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 fi
if [ "${RUN_JS_TESTS}" = "true" ]; then if [ "${RUN_JS_TESTS}" = "true" ]; then

View File

@ -72,15 +72,18 @@ $CONTAINER_IMAGE \
# Use curl to wait for application container to become available # Use curl to wait for application container to become available
docker pull curlimages/curl:latest docker pull curlimages/curl:latest
echo "Waiting for application container to become available"
docker run --network atat \ docker run --network atat \
curlimages/curl:latest \ curlimages/curl:latest \
curl --connect-timeout 3 \ curl \
--silent \
--connect-timeout 3 \
--max-time 5 \ --max-time 5 \
--retry $CONTAINER_TIMEOUT \ --retry $CONTAINER_TIMEOUT \
--retry-connrefused \ --retry-connrefused \
--retry-delay 1 \ --retry-delay 1 \
--retry-max-time $CONTAINER_TIMEOUT \ --retry-max-time $CONTAINER_TIMEOUT \
test-atat:8000 test-atat:8000 >/dev/null
# Run Ghost Inspector tests # Run Ghost Inspector tests
docker pull ghostinspector/test-runner-standalone:latest 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 atst.utils import pick
from tests.factories import ( from tests.factories import (
random_service_branch, random_defense_component,
TaskOrderFactory, TaskOrderFactory,
CLINFactory, CLINFactory,
AttachmentFactory, AttachmentFactory,
@ -159,7 +159,7 @@ def get_users():
def add_members_to_portfolio(portfolio): def add_members_to_portfolio(portfolio):
for user_data in PORTFOLIO_USERS: 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 = { profile = {
k: user_data[k] for k in user_data if k not in ["dod_id", "permission_sets"] 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( portfolio = Portfolios.create(
user=portfolio_owner, 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) add_task_orders_to_portfolio(portfolio)
@ -336,7 +336,7 @@ def seed_db():
user=amanda, user=amanda,
portfolio_attrs={ portfolio_attrs={
"name": "TIE Interceptor", "name": "TIE Interceptor",
"defense_component": random_service_branch(), "defense_component": random_defense_component(),
}, },
) )
add_task_orders_to_portfolio(tie_interceptor) add_task_orders_to_portfolio(tie_interceptor)
@ -347,7 +347,7 @@ def seed_db():
user=amanda, user=amanda,
portfolio_attrs={ portfolio_attrs={
"name": "TIE Fighter", "name": "TIE Fighter",
"defense_component": random_service_branch(), "defense_component": random_defense_component(),
}, },
) )
add_task_orders_to_portfolio(tie_fighter) add_task_orders_to_portfolio(tie_fighter)
@ -363,7 +363,7 @@ def seed_db():
user=user, user=user,
portfolio_attrs={ portfolio_attrs={
"name": ship, "name": ship,
"defense_component": random_service_branch(), "defense_component": random_defense_component(),
}, },
) )
add_task_orders_to_portfolio(portfolio) 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/dod_login_notice.scss";
@import "components/sticky_cta.scss"; @import "components/sticky_cta.scss";
@import "components/error_page.scss"; @import "components/error_page.scss";
@import "components/member_form.scss";
@import "sections/login"; @import "sections/login";
@import "sections/home"; @import "sections/home";

View File

@ -3,8 +3,7 @@
background-color: $color-white; background-color: $color-white;
border-top: 1px solid $color-gray-lightest; border-top: 1px solid $color-gray-lightest;
display: flex; display: flex;
flex-direction: row; flex-direction: row-reverse;
justify-content: space-between;
align-items: center; align-items: center;
padding: $gap * 1.5; padding: $gap * 1.5;
position: fixed; position: fixed;
@ -15,19 +14,7 @@
color: $color-gray-dark; color: $color-gray-dark;
font-size: 1.5rem; font-size: 1.5rem;
&__info { &__login {
flex-grow: 1;
padding-left: 0.8rem; 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 Grid
.form-row { .form-row {
margin: ($gap * 4) 0; margin: ($gap * 4) 0;
&--separated {
&--bordered {
border-bottom: $color-gray-lighter 1px solid; border-bottom: $color-gray-lighter 1px solid;
} }
@ -202,3 +203,7 @@
display: inline; display: inline;
} }
} }
.form-container__half {
max-width: 46rem;
}

View File

@ -4,18 +4,4 @@
height: auto; height: auto;
box-shadow: $box-shadow; box-shadow: $box-shadow;
margin-bottom: -$footer-height * 2.5; 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; margin-left: 2 * $gap;
.line {
box-sizing: border-box;
height: 2px;
width: 100%;
border: 1px solid $color-gray-lightest;
}
} }
.portfolio-header { .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 { .links {
justify-content: center; justify-content: center;
font-size: $small-font-size; 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 { @mixin subheading {
@ -138,6 +85,10 @@
.portfolio-content { .portfolio-content {
margin: (4 * $gap) $gap 0 $gap; margin: (4 * $gap) $gap 0 $gap;
.panel {
padding-bottom: 2rem;
}
a.add-new-button { a.add-new-button {
display: inherit; display: inherit;
margin-left: auto; 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 { a.modal-link.icon-link {
float: right; float: right;
@ -607,3 +520,28 @@
margin-right: $gap * 3; 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-size: $small-font-size;
font-weight: $font-bold; font-weight: $font-bold;
} }
&--link {
font-weight: $font-bold;
}
} }

View File

@ -12,9 +12,10 @@
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
justify-content: space-between; justify-content: space-between;
}
.topbar__link { &__link {
color: $color-white; color: $color-white !important;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
height: $topbar-height; height: $topbar-height;
@ -38,62 +39,17 @@
padding-left: $gap / 2; padding-left: $gap / 2;
} }
&--shield {
width: $icon-bar-width;
justify-content: center;
padding: 0;
.topbar__link-icon {
margin: 0;
}
}
&:hover { &:hover {
background-color: $color-primary-darker; background-color: $color-primary-darker;
color: $color-white; color: $color-white;
} }
} }
.topbar__context { &__context {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
justify-content: flex-end; justify-content: flex-end;
.topbar__portfolio-menu {
margin-right: auto;
position: relative;
}
}
}
&.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;
}
}
} }
} }

View File

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

View File

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

View File

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

View File

@ -94,4 +94,19 @@
&--primary { &--primary {
@include icon-color($color-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-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
label {
margin-left: 3rem;
&:before {
position: absolute;
left: -3rem;
}
}
} }
select { select {

View File

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

View File

@ -21,24 +21,8 @@ table.atat-table {
text-align: right; text-align: right;
} }
&--align-center { &--third {
text-align: center; width: 33%;
}
&--shrink {
width: 1%;
}
&--expand {
width: 100%;
}
&--hide-small {
display: none;
@include media($medium-screen) {
display: table-cell;
}
} }
} }
} }
@ -53,7 +37,6 @@ table.atat-table {
padding: $gap * 2; padding: $gap * 2;
border: 1px solid $color-gray-lighter; border: 1px solid $color-gray-lighter;
display: table-cell; display: table-cell;
white-space: nowrap;
vertical-align: top; vertical-align: top;
&:first-child { &:first-child {
@ -84,28 +67,6 @@ table.atat-table {
@include panel-margin; @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 { table {
margin-bottom: 0; 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 { .environment-roles {
padding: 0 ($gap * 3) ($gap * 3); padding: 0 ($gap * 3) ($gap * 3);

View File

@ -1,48 +1,24 @@
.home { .home {
margin: $gap * 3;
.sticky-cta { .sticky-cta {
margin: -1.6rem -1.6rem 0 -1.6rem; margin: -1.6rem -1.6rem 0 -1.6rem;
} }
}
.about-cloud { &__content {
margin: 4rem auto; margin: 4rem;
max-width: 900px; max-width: 900px;
}
.your-project { &--descriptions {
margin-top: 3rem; .col {
padding: 3rem; margin-left: $home-pg-icon-width;
background-color: $color-gray-lightest; padding: ($gap * 2) ($gap * 4);
position: relative;
h2 { .icon--home-pg-badge {
margin-top: 0; position: absolute;
left: -$home-pg-icon-width;
top: $gap * 3;
} }
.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;
}
}
&.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 { .reporting-expended-funding {
&__header { &__header {
margin: 0; margin: 0;

View File

@ -20,10 +20,7 @@
} }
&__header { &__header {
.h2, margin-bottom: $gap * 6;
p {
margin-bottom: $gap * 0.5;
}
} }
.col { .col {
@ -39,22 +36,6 @@
margin-top: $gap * 2; 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 { .card {
padding: ($gap * 4) ($gap * 5) 0; padding: ($gap * 4) ($gap * 5) 0;
@ -155,6 +136,10 @@
} }
} }
} }
&__confirmation {
margin-left: $gap * 8;
}
} }
.task-order__modal-cancel { .task-order__modal-cancel {
@ -170,3 +155,16 @@
padding-bottom: $gap * 2.5; 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_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) }} {{ CheckboxInput(form.perms_del_env, classes="input__inline-fields", key=del_env, id=del_env, optional=True) }}
</div> </div>
<hr> <hr class="full-width">
<div class="environment_roles environment-roles-new"> <div class="environment_roles environment-roles-new">
<h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2> <h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2>
<p class='usa-input__help subtitle'> <p class='usa-input__help subtitle'>
@ -118,7 +118,7 @@
{% endmacro %} {% endmacro %}
{% macro InfoFields(member_form) %} {% 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.first_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }} {{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
{{ TextInput(member_form.email, validation='email', 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/icon.html" import Icon %}
{% from "components/label.html" import Label %} {% 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 %} {% import "applications/fragments/member_form_fields.html" as member_fields %}
{% from "components/modal.html" import Modal %} {% from "components/modal.html" import Modal %}
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %} {% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% from "components/save_button.html" import SaveButton %} {% from "components/save_button.html" import SaveButton %}
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{% macro MemberManagementTemplate( {% macro MemberManagementTemplate(
application, application,
@ -40,7 +38,7 @@
{% call Modal(modal_name, classes="form-content--app-mem") %} {% call Modal(modal_name, classes="form-content--app-mem") %}
<div class="modal__form--header"> <div class="modal__form--header">
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1> <h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
<hr> <hr class="full-width">
</div> </div>
<base-form inline-template> <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,) }}"> <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") %} {% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
<div class="modal__form--header"> <div class="modal__form--header">
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1> <h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
<hr> <hr class="full-width">
</div> </div>
<base-form inline-template :enable-save="true"> <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) }}"> <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=new_member_form,
form_action=url_for(action_new, application_id=application.id), form_action=url_for(action_new, application_id=application.id),
steps=[ steps=[
member_steps.MemberStepOne(new_member_form), member_form.BasicStep(
member_steps.MemberStepTwo(new_member_form, application) 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 %} {% 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) }} {{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
</div> </div>
</div> </div>
<hr class="panel__break"> <hr>
<div class="form-row"> <div class="form-row">
<div class="form-col form-col--two-thirds"> <div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=True) }} {{ TextInput(form.description, paragraph=True, optional=True) }}

View File

@ -19,7 +19,7 @@
<p> <p>
{{ 'portfolios.applications.new.step_2_description' | translate }} {{ 'portfolios.applications.new.step_2_description' | translate }}
</p> </p>
<hr class="panel__break"> <hr>
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'> <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"> <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> <div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>

View File

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

View File

@ -13,6 +13,9 @@
{% block application_content %} {% block application_content %}
{% if show_flash -%}
{% include "fragments/flash.html" %}
{%- endif %}
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3> <h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
{% if user_can(permissions.EDIT_APPLICATION) %} {% if user_can(permissions.EDIT_APPLICATION) %}
@ -59,59 +62,8 @@
environments_obj, environments_obj,
new_env_form) }} 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) %} {% if user_can(permissions.VIEW_APPLICATION_ACTIVITY_LOG) and config.get("USE_AUDIT_LOG", False) %}
<hr>
{% include "fragments/audit_events_log.html" %} {% include "fragments/audit_events_log.html" %}
{{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }} {{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }}
{% endif %} {% endif %}

View File

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

View File

@ -9,11 +9,15 @@
"text": "changes pending", "text": "changes pending",
"color": "default", "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 }}'> <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> </span>
{%- endif %} {%- endif %}
{%- endmacro %} {%- 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) -%}
<li class="sidenav__item">
{% 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}}"> <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"> <span class="sidenav__link-label">
{{label}} {{label}}
</span> </span>
{% if active %}
<span class="sidenav__link-active_indicator">
{{ Icon("caret_right") }}
</span>
{% endif %}
</a> </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 %}
</li> </li>
{%- endmacro %} {%- endmacro %}

View File

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

View File

@ -1,14 +1,8 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
<footer class='app-footer'> <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 %} {% 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> {{ "footer.login" | translate }} <local-datetime timestamp='{{ g.last_login }}' format='MMM D YYYY H:mm Z'></local-datetime>
</div> </div>
{% endif %} {% endif %}

View File

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

View File

@ -1,8 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %} {% from "components/sticky_cta.html" import StickyCTA %}
{% block content %} {% block content %}
@ -13,88 +12,56 @@
{% set sticky_header = "home.get_started" | translate %} {% set sticky_header = "home.get_started" | translate %}
{% endif %} {% endif %}
{% call StickyCTA(sticky_header) %} <div class="home__content">
<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">
{% include "fragments/flash.html" %} {% include "fragments/flash.html" %}
<h1>{{ "home.head" | translate }}</h1> <h1>{{ "home.head" | translate }}</h1>
<h3>Set up a Portfolio</h3>
{{ SemiCollapsibleText(first_half=("home.about_cloud.part1"|translate), second_half=("home.about_cloud.part2"|translate)) }} <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>
<div class="your-project">
<h2 class="h3">{{ "home.your_project" | translate }}</h2>
<p>{{ "home.your_project_descrip" | translate }}</p>
<hr> <hr>
{% macro Link(icon, text, section, default=False) %} <div class="home__content--descriptions">
{% if default %} <div class="row">
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}" || selectedSection === null}' v-on:click="toggleSection('{{ section }}')"> <div class="col col--half col--pad">
{% else %} {{ Icon('funding', classes="icon--home-pg-badge") }}
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}"}' v-on:click="toggleSection('{{ section }}')"> <h4>{{ "navigation.portfolio_navigation.breadcrumbs.funding" | translate }}</h4>
{% endif %} <p>
<div class="col"> {{ "home.funding_descrip" | translate }}
<div class='icon-link--icon'>{{ Icon(icon) }}</div> </p>
<div class='icon-link--name'>{{ text }}</div> </div>
</div> <div class="col col--half col--pad">
</div> {{ Icon('chart-pie', classes="icon--home-pg-badge") }}
{% endmacro %} <h4>{{ "navigation.portfolio_navigation.breadcrumbs.reports" | translate }}</h4>
<p>
<toggler inline-template v-bind:initial-selected-section="'funding'"> {{ "home.reports_descrip" | translate }}
<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> </p>
{% endmacro %}
<div class="project-section-descriptions">
{{ Description('funding', default=True) }}
{{ Description('applications') }}
{{ Description('reports') }}
{{ Description('admin') }}
</div> </div>
</div> </div>
</toggler> <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> </div>
<img id='jedi-heirarchy' src="{{ url_for("static", filename="img/JEDIhierarchyDiagram.png")}}" alt="JEDI heirarchy diagram">
</div> </div>
</main> </main>

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