Merge branch 'staging' of github-DDS:dod-ccpo/atst into gi-updates-wo-20191209

This commit is contained in:
Jay R. Newlin (PromptWorks)
2019-12-13 13:52:24 -05:00
27 changed files with 504 additions and 286 deletions

View File

@@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null "lines": null
}, },
"generated_at": "2019-12-05T17:54:05Z", "generated_at": "2019-12-06T21:22:07Z",
"plugins_used": [ "plugins_used": [
{ {
"base64_limit": 4.5, "base64_limit": 4.5,
@@ -161,7 +161,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 31, "line_number": 41,
"type": "Hex High Entropy String" "type": "Hex High Entropy String"
} }
], ],

View File

@@ -29,6 +29,7 @@ azure-mgmt-subscription = "*"
azure-graphrbac = "*" azure-graphrbac = "*"
msrestazure = "*" msrestazure = "*"
azure-mgmt-authorization = "*" azure-mgmt-authorization = "*"
azure-mgmt-managementgroups = "*"
[dev-packages] [dev-packages]
bandit = "*" bandit = "*"

177
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "6d2ab855267daac877ae7464de9dba5b62b7d89288992f87d8fc6ff0c0d2520f" "sha256": "c2b19c436646705ea3bf4df8c35c2833083f048da37fc619e66f7236153607c5"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -39,11 +39,11 @@
}, },
"apache-libcloud": { "apache-libcloud": {
"hashes": [ "hashes": [
"sha256:201751f738109f25d58dcdfb5804e17216e0dc8f68b522e9e26ac16e0b9ff2ea", "sha256:9bc5cd5c32151bb7a04a7c7de0be9b4a4b8271e348ac91dd79eaaeeae627115f",
"sha256:40215db1bd489d17dc1abfdb289d7f035313c7297b6a7462c79d8287cbbeae91" "sha256:fcc165f2cc2db9a379c6d3a17b3beb9081bb64ba5c0bf7bbb58da864810092f0"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.6.0" "version": "==2.6.1"
}, },
"azure-common": { "azure-common": {
"hashes": [ "hashes": [
@@ -68,6 +68,14 @@
"index": "pypi", "index": "pypi",
"version": "==0.60.0" "version": "==0.60.0"
}, },
"azure-mgmt-managementgroups": {
"hashes": [
"sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682",
"sha256:8194ee6274df865eccd1ed9d385ea625aeba9b8058b9e4fdf547f5207271a775"
],
"index": "pypi",
"version": "==0.2.0"
},
"azure-mgmt-subscription": { "azure-mgmt-subscription": {
"hashes": [ "hashes": [
"sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1", "sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1",
@@ -117,10 +125,10 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
], ],
"version": "==2019.9.11" "version": "==2019.11.28"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
@@ -248,10 +256,10 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" "sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
], ],
"version": "==0.23" "version": "==1.1.0"
}, },
"isodate": { "isodate": {
"hashes": [ "hashes": [
@@ -330,10 +338,10 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" "sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
], ],
"version": "==7.2.0" "version": "==8.0.0"
}, },
"msrest": { "msrest": {
"hashes": [ "hashes": [
@@ -422,11 +430,11 @@
}, },
"pyopenssl": { "pyopenssl": {
"hashes": [ "hashes": [
"sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200", "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504",
"sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6" "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507"
], ],
"index": "pypi", "index": "pypi",
"version": "==19.0.0" "version": "==19.1.0"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
@@ -459,22 +467,20 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.1.2" "version": "==5.2"
}, },
"redis": { "redis": {
"hashes": [ "hashes": [
@@ -650,10 +656,10 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
], ],
"version": "==2019.9.11" "version": "==2019.11.28"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@@ -785,10 +791,10 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" "sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
], ],
"version": "==0.23" "version": "==1.1.0"
}, },
"ipdb": { "ipdb": {
"hashes": [ "hashes": [
@@ -799,11 +805,11 @@
}, },
"ipython": { "ipython": {
"hashes": [ "hashes": [
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280", "sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51",
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995" "sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e"
], ],
"index": "pypi", "index": "pypi",
"version": "==7.9.0" "version": "==7.10.1"
}, },
"ipython-genutils": { "ipython-genutils": {
"hashes": [ "hashes": [
@@ -908,30 +914,30 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" "sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
], ],
"version": "==7.2.0" "version": "==8.0.0"
}, },
"mypy": { "mypy": {
"hashes": [ "hashes": [
"sha256:1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f", "sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768",
"sha256:31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00", "sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646",
"sha256:3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae", "sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c",
"sha256:48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d", "sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939",
"sha256:540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9", "sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279",
"sha256:672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391", "sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2",
"sha256:6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f", "sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2",
"sha256:9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9", "sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640",
"sha256:ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990", "sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7",
"sha256:b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7", "sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c",
"sha256:d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb", "sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17",
"sha256:d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453", "sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78",
"sha256:dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b", "sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931",
"sha256:f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb" "sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.740" "version": "==0.750"
}, },
"mypy-extensions": { "mypy-extensions": {
"hashes": [ "hashes": [
@@ -961,10 +967,10 @@
}, },
"pbr": { "pbr": {
"hashes": [ "hashes": [
"sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
"sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"
], ],
"version": "==5.4.3" "version": "==5.4.4"
}, },
"pexpect": { "pexpect": {
"hashes": [ "hashes": [
@@ -983,18 +989,17 @@
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
], ],
"version": "==0.13.0" "version": "==0.13.1"
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", "sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7",
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", "sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990"
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
], ],
"version": "==2.0.10" "version": "==3.0.2"
}, },
"ptyprocess": { "ptyprocess": {
"hashes": [ "hashes": [
@@ -1012,10 +1017,10 @@
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
], ],
"version": "==2.4.2" "version": "==2.5.2"
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
@@ -1058,11 +1063,11 @@
}, },
"pytest-mock": { "pytest-mock": {
"hashes": [ "hashes": [
"sha256:b3514caac35fe3f05555923eabd9546abce11571cc2ddf7d8615959d04f2c89e", "sha256:96a0cebc66e09930be2a15b03333d90b59584d3fb011924f81c14b50ee0afbba",
"sha256:ea502c3891599c26243a3a847ccf0b1d20556678c528f86c98e3cd6d40c5cf11" "sha256:e5381be2608e49547f5e47633c5f81241ebf6206d17ce516a7a18d5a917e3859"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.11.2" "version": "==1.12.1"
}, },
"pytest-watch": { "pytest-watch": {
"hashes": [ "hashes": [
@@ -1080,22 +1085,20 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.1.2" "version": "==5.2"
}, },
"regex": { "regex": {
"hashes": [ "hashes": [

View File

@@ -3,6 +3,7 @@ import re
from uuid import uuid4 from uuid import uuid4
from atst.models.user import User from atst.models.user import User
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
@@ -399,13 +400,14 @@ 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 from azure.mgmt import subscription, authorization, managementgroups
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
self.subscription = subscription self.subscription = subscription
self.authorization = authorization self.authorization = authorization
self.managementgroups = managementgroups
self.graphrbac = graphrbac self.graphrbac = graphrbac
self.credentials = credentials self.credentials = credentials
# may change to a JEDI cloud # may change to a JEDI cloud
@@ -428,42 +430,23 @@ class AzureCloudProvider(CloudProviderInterface):
def create_environment( def create_environment(
self, auth_credentials: Dict, user: User, environment: Environment self, auth_credentials: Dict, user: User, environment: Environment
): ):
# since this operation would only occur within a tenant, should we source the tenant
# via lookup from environment once we've created the portfolio csp data schema
# something like this:
# environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None)
# though we'd probably source the whole credentials for these calls from the portfolio csp
# data, as it would have to be where we store the creds for the at-at user within the portfolio tenant
# credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds())
credentials = self._get_credential_obj(self._root_creds) credentials = self._get_credential_obj(self._root_creds)
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
management_group_id = "?" # management group id chained from environment
parent_id = "?" # from environment.application
billing_profile_id = "?" # something chained from environment? management_group = self._create_management_group(
sku_id = AZURE_SKU_ID credentials, management_group_id, display_name, parent_id,
# we want to set AT-AT as an owner here
# we could potentially associate subscriptions with "management groups" per DOD component
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
display_name,
billing_profile_id,
sku_id,
# owner=<AdPrincipal: for AT-AT user>
) )
# These 2 seem like something that might be worthwhile to allow tiebacks to return management_group
# TOs filed for the environment
billing_account_name = "?"
invoice_section_name = "?"
# We may also want to create billing sections in the enrollment account
sub_creation_operation = sub_client.subscription_factory.create_subscription(
billing_account_name, invoice_section_name, body
)
# the resulting object from this process is a link to the new subscription
# not a subscription model, so we'll have to unpack the ID
new_sub = sub_creation_operation.result()
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
if subscription_id:
return subscription_id
else:
# troublesome error, subscription should exist at this point
# but we just don't have a valid ID
pass
def create_atat_admin_user( def create_atat_admin_user(
self, auth_credentials: Dict, csp_environment_id: str self, auth_credentials: Dict, csp_environment_id: str
@@ -502,6 +485,83 @@ class AzureCloudProvider(CloudProviderInterface):
"role_name": role_assignment_id, "role_name": role_assignment_id,
} }
def _create_application(self, auth_credentials: Dict, application: Application):
management_group_name = str(uuid4()) # can be anything, not just uuid
display_name = application.name # Does this need to be unique?
credentials = self._get_credential_obj(auth_credentials)
parent_id = "?" # application.portfolio.csp_details.management_group_id
return self._create_management_group(
credentials, management_group_name, display_name, parent_id,
)
def _create_management_group(
self, credentials, management_group_id, display_name, parent_id=None,
):
mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials)
create_parent_grp_info = self.sdk.managementgroups.models.CreateParentGroupInfo(
id=parent_id
)
create_mgmt_grp_details = self.sdk.managementgroups.models.CreateManagementGroupDetails(
parent=create_parent_grp_info
)
mgmt_grp_create = self.sdk.managementgroups.models.CreateManagementGroupRequest(
name=management_group_id,
display_name=display_name,
details=create_mgmt_grp_details,
)
create_request = mgmgt_group_client.management_groups.create_or_update(
management_group_id, mgmt_grp_create
)
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally
return create_request.result()
def _create_subscription(
self,
credentials,
display_name,
billing_profile_id,
sku_id,
management_group_id,
billing_account_name,
invoice_section_name,
):
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
billing_profile_id = "?" # where do we source this?
sku_id = AZURE_SKU_ID
# These 2 seem like something that might be worthwhile to allow tiebacks to
# TOs filed for the environment
billing_account_name = "?" # from TO?
invoice_section_name = "?" # from TO?
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
display_name=display_name,
billing_profile_id=billing_profile_id,
sku_id=sku_id,
management_group_id=management_group_id,
)
# We may also want to create billing sections in the enrollment account
sub_creation_operation = sub_client.subscription_factory.create_subscription(
billing_account_name, invoice_section_name, body
)
# the resulting object from this process is a link to the new subscription
# not a subscription model, so we'll have to unpack the ID
new_sub = sub_creation_operation.result()
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
if subscription_id:
return subscription_id
else:
# troublesome error, subscription should exist at this point
# but we just don't have a valid ID
pass
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

View File

@@ -64,10 +64,12 @@ class TaskOrders(BaseDomainClass):
db.session.commit() db.session.commit()
@classmethod @classmethod
def sort(cls, task_orders: [TaskOrder]) -> [TaskOrder]: def sort_by_status(cls, task_orders):
# Sorts a list of task orders on two keys: status (primary) and time_created (secondary) by_status = {status.value: [] for status in SORT_ORDERING}
by_time_created = sorted(task_orders, key=lambda to: to.time_created)
by_status = sorted(by_time_created, key=lambda to: SORT_ORDERING.get(to.status)) for task_order in task_orders:
by_status[task_order.display_status].append(task_order)
return by_status return by_status
@classmethod @classmethod

View File

@@ -1,5 +1,5 @@
from datetime import timedelta
from enum import Enum from enum import Enum
from decimal import Decimal
from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy import Column, DateTime, ForeignKey, String
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
@@ -17,15 +17,16 @@ class Status(Enum):
ACTIVE = "Active" ACTIVE = "Active"
UPCOMING = "Upcoming" UPCOMING = "Upcoming"
EXPIRED = "Expired" EXPIRED = "Expired"
UNSIGNED = "Not signed" UNSIGNED = "Unsigned"
SORT_ORDERING = { SORT_ORDERING = [
status: order Status.ACTIVE,
for (order, status) in enumerate( Status.DRAFT,
[Status.DRAFT, Status.ACTIVE, Status.UPCOMING, Status.EXPIRED, Status.UNSIGNED] Status.UPCOMING,
) Status.EXPIRED,
} Status.UNSIGNED,
]
class TaskOrder(Base, mixins.TimestampsMixin): class TaskOrder(Base, mixins.TimestampsMixin):
@@ -131,12 +132,11 @@ class TaskOrder(Base, mixins.TimestampsMixin):
@property @property
def start_date(self): def start_date(self):
return min((c.start_date for c in self.clins), default=self.time_created.date()) return min((c.start_date for c in self.clins), default=None)
@property @property
def end_date(self): def end_date(self):
default_end_date = self.start_date + timedelta(days=1) return max((c.end_date for c in self.clins), default=None)
return max((c.end_date for c in self.clins), default=default_end_date)
@property @property
def days_to_expiration(self): def days_to_expiration(self):
@@ -170,6 +170,11 @@ class TaskOrder(Base, mixins.TimestampsMixin):
# Faked for display purposes # Faked for display purposes
return 50 return 50
@property
def invoiced_funds(self):
# TODO: implement this using reporting data from the CSP
return self.total_obligated_funds * Decimal(0.75)
@property @property
def display_status(self): def display_status(self):
return self.status.value return self.status.value

View File

@@ -20,6 +20,7 @@ from atst.domain.permission_sets import PermissionSets
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
from atst.utils.localization import translate from atst.utils.localization import translate
from atst.jobs import send_mail from atst.jobs import send_mail
from atst.routes.errors import log_error
def get_environments_obj_for_app(application): def get_environments_obj_for_app(application):
@@ -234,7 +235,8 @@ def handle_update_member(application_id, application_role_id, form_data):
flash("application_member_updated", user_name=app_role.user_name) flash("application_member_updated", user_name=app_role.user_name)
except GeneralCSPException: except GeneralCSPException as exc:
log_error(exc)
flash( flash(
"application_member_update_error", user_name=app_role.user_name, "application_member_update_error", user_name=app_role.user_name,
) )

View File

@@ -6,7 +6,6 @@ from atst.domain.portfolios import Portfolios
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders
from atst.forms.task_order import SignatureForm from atst.forms.task_order import SignatureForm
from atst.models import Permissions from atst.models import Permissions
from atst.models.task_order import Status as TaskOrderStatus
@task_orders_bp.route("/task_orders/<task_order_id>/review") @task_orders_bp.route("/task_orders/<task_order_id>/review")
@@ -28,14 +27,9 @@ def review_task_order(task_order_id):
@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding") @user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding")
def portfolio_funding(portfolio_id): def portfolio_funding(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id) portfolio = Portfolios.get(g.current_user, portfolio_id)
task_orders = TaskOrders.sort(portfolio.task_orders) task_orders = TaskOrders.sort_by_status(portfolio.task_orders)
label_colors = { to_count = len(portfolio.task_orders)
TaskOrderStatus.DRAFT: "warning", # TODO: Get expended amount from the CSP
TaskOrderStatus.ACTIVE: "success",
TaskOrderStatus.UPCOMING: "info",
TaskOrderStatus.EXPIRED: "error",
TaskOrderStatus.UNSIGNED: "purple",
}
return render_template( return render_template(
"task_orders/index.html", task_orders=task_orders, label_colors=label_colors "task_orders/index.html", task_orders=task_orders, to_count=to_count
) )

View File

@@ -13,10 +13,10 @@ data:
CDN_ORIGIN: https://azure.atat.code.mil CDN_ORIGIN: https://azure.atat.code.mil
CELERY_DEFAULT_QUEUE: celery-master CELERY_DEFAULT_QUEUE: celery-master
CSP: azure CSP: azure
DEBUG: 0 DEBUG: "0"
FLASK_ENV: master FLASK_ENV: master
LOG_JSON: "true" LOG_JSON: "true"
MAIL_PORT: 587 MAIL_PORT: "587"
MAIL_SENDER: postmaster@atat.code.mil MAIL_SENDER: postmaster@atat.code.mil
MAIL_SERVER: smtp.mailgun.org MAIL_SERVER: smtp.mailgun.org
MAIL_TLS: "true" MAIL_TLS: "true"
@@ -24,7 +24,7 @@ data:
PGAPPNAME: atst PGAPPNAME: atst
PGDATABASE: staging PGDATABASE: staging
PGHOST: atat-db.postgres.database.azure.com PGHOST: atat-db.postgres.database.azure.com
PGPORT: 5432 PGPORT: "5432"
PGSSLMODE: verify-full PGSSLMODE: verify-full
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
PGUSER: atat_master@atat-db PGUSER: atat_master@atat-db

View File

@@ -9,9 +9,9 @@ data:
AZURE_TO_BUCKET_NAME: task-order-pdfs AZURE_TO_BUCKET_NAME: task-order-pdfs
CAC_URL: https://auth-staging.atat.code.mil/login-redirect CAC_URL: https://auth-staging.atat.code.mil/login-redirect
CELERY_DEFAULT_QUEUE: celery-master CELERY_DEFAULT_QUEUE: celery-master
DEBUG: 0 DEBUG: "0"
DISABLE_CRL_CHECK: "true" DISABLE_CRL_CHECK: "true"
MAIL_PORT: 587 MAIL_PORT: "587"
MAIL_SENDER: postmaster@atat.code.mil MAIL_SENDER: postmaster@atat.code.mil
MAIL_SERVER: smtp.mailgun.org MAIL_SERVER: smtp.mailgun.org
MAIL_TLS: "true" MAIL_TLS: "true"
@@ -19,7 +19,7 @@ data:
PGAPPNAME: atst PGAPPNAME: atst
PGDATABASE: staging PGDATABASE: staging
PGHOST: atat-db.postgres.database.azure.com PGHOST: atat-db.postgres.database.azure.com
PGPORT: 5432 PGPORT: "5432"
PGSSLMODE: verify-full PGSSLMODE: verify-full
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
PGUSER: atat_master@atat-db PGUSER: atat_master@atat-db

View File

@@ -11,4 +11,10 @@ export default {
default: false, default: false,
}, },
}, },
methods: {
collapse: function() {
this.isVisible = false
},
},
} }

View File

@@ -0,0 +1,16 @@
import Accordion from './accordion'
export default {
name: 'accordion-list',
components: {
Accordion,
},
methods: {
handleClick: function(e) {
e.preventDefault()
this.$children.forEach(el => el.collapse())
},
},
}

View File

@@ -7,6 +7,8 @@ import Vue from 'vue/dist/vue'
import VTooltip from 'v-tooltip' import VTooltip from 'v-tooltip'
import stickybits from 'stickybits' import stickybits from 'stickybits'
import Accordion from './components/accordion'
import AccordionList from './components/accordion_list'
import dodlogin from './components/dodlogin' import dodlogin from './components/dodlogin'
import optionsinput from './components/options_input' import optionsinput from './components/options_input'
import multicheckboxinput from './components/multi_checkbox_input' import multicheckboxinput from './components/multi_checkbox_input'
@@ -29,7 +31,6 @@ import SemiCollapsibleText from './components/semi_collapsible_text'
import ToForm from './components/forms/to_form' import ToForm from './components/forms/to_form'
import ClinFields from './components/clin_fields' import ClinFields from './components/clin_fields'
import PopDateRange from './components/pop_date_range' import PopDateRange from './components/pop_date_range'
import Accordion from './components/accordion'
import ToggleMenu from './components/toggle_menu' import ToggleMenu from './components/toggle_menu'
Vue.config.productionTip = false Vue.config.productionTip = false
@@ -42,6 +43,7 @@ const app = new Vue({
el: '#app-root', el: '#app-root',
components: { components: {
Accordion, Accordion,
AccordionList,
dodlogin, dodlogin,
toggler, toggler,
optionsinput, optionsinput,

View File

@@ -17,6 +17,7 @@ export default {
methods: { methods: {
toggle: function(e) { toggle: function(e) {
e.preventDefault() e.preventDefault()
e.stopPropagation()
this.isVisible = !this.isVisible this.isVisible = !this.isVisible
}, },
}, },

View File

@@ -46,5 +46,20 @@
margin: 0; margin: 0;
} }
} }
&--empty {
font-weight: $font-bold;
color: $color-gray-dark;
padding: $gap * 8;
text-align: center;
}
}
&-list {
max-width: $max-panel-width;
&__collapse {
cursor: pointer;
}
} }
} }

View File

@@ -127,21 +127,6 @@
width: 100%; width: 100%;
} }
.label {
&--pending,
&--started {
background-color: $color-gold;
}
&--active {
background-color: $color-green;
}
&--expired {
background-color: $color-red;
}
}
.task-order-document-link { .task-order-document-link {
&__icon { &__icon {
padding-top: 0.5rem; padding-top: 0.5rem;

View File

@@ -1,4 +1,5 @@
{% from "components/accordion.html" import Accordion %} {% from "components/accordion.html" import Accordion %}
{% from "components/accordion_list.html" import AccordionList %}
{% from "components/empty_state.html" import EmptyState %} {% from "components/empty_state.html" import EmptyState %}
{% from "components/sticky_cta.html" import StickyCTA %} {% from "components/sticky_cta.html" import StickyCTA %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
@@ -32,7 +33,7 @@
) }} ) }}
{% else %} {% else %}
<div class="usa-accordion"> {% call AccordionList() %}
{% for application in portfolio.applications|sort(attribute='name') %} {% for application in portfolio.applications|sort(attribute='name') %}
{% set section_name = "application-{}".format(application.id) %} {% set section_name = "application-{}".format(application.id) %}
{% set title = "Environments ({})".format(application.environments|length) %} {% set title = "Environments ({})".format(application.environments|length) %}
@@ -76,7 +77,7 @@
{% endcall %} {% endcall %}
</div> </div>
{% endfor %} {% endfor %}
</div> {% endcall %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -0,0 +1,11 @@
{% macro AccordionList() %}
<accordion-list inline-template>
<div class="accordion-list usa-accordion">
<div class="action-group">
<a v-on:click="handleClick($event)" class="accordion-list__collapse">Collapse All</a>
</div>
<!-- caller iterates over accordion vue components or Accordion jinja macros -->
{{ caller() }}
</div>
</accordion-list>
{% endmacro %}

View File

@@ -1,4 +1,5 @@
{% from "components/accordion.html" import Accordion %} {% from "components/accordion.html" import Accordion %}
{% from "components/accordion_list.html" import AccordionList %}
{% from "components/empty_state.html" import EmptyState %} {% from "components/empty_state.html" import EmptyState %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/sticky_cta.html" import StickyCTA %} {% from "components/sticky_cta.html" import StickyCTA %}
@@ -13,38 +14,52 @@
{% macro TaskOrderList(task_orders, status) %} {% macro TaskOrderList(task_orders, status) %}
{% set status = "All Task Orders" %} <div class="accordion">
<div class="accordion usa-accordion"> {% call Accordion(title=("task_orders.status_list_title"|translate({'status': status})), id=status, heading_tag="h4") %}
{% call Accordion(title=status, id=status, heading_tag="h4") %} {% if task_orders|length > 0 %}
{% for task_order in task_orders %} {% for task_order in task_orders %}
<div class="accordion__content--list-item"> {% set to_number %}
<h4><a href="{{ url_for('task_orders.review_task_order', task_order_id=task_order.id) }}">Task Order #{{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}</a></h4> {% if task_order.number != "" %}
<div class="row"> Task Order #{{ task_order.number }}
<div class="col col--grow"> {% else %}
<h5> New Task Order
Current Period of Performance {% endif %}
</h5> {% endset %}
<p> <div class="accordion__content--list-item">
{{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }} <h4><a href="{{ url_for('task_orders.review_task_order', task_order_id=task_order.id) }}">{{ to_number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}</a></h4>
- {% if status != 'Expired' -%}
{{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }} <div class="row">
</p> <div class="col col--grow">
</div> <h5>
<div class="col col--grow"> Current Period of Performance
<h5>Total Value</h5> </h5>
<p>{{ task_order.total_contract_amount | dollars }}</p> <p>
</div> {{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }}
<div class="col col--grow"> -
<h5>Total Obligated</h5> {{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }}
<p>{{ task_order.total_obligated_funds | dollars }}</p> </p>
</div> </div>
<div class="col col--grow"> <div class="col col--grow">
<h5>Total Expended</h5> <h5>Total Value</h5>
<p>$0</p> <p>{{ task_order.total_contract_amount | dollars }}</p>
</div> </div>
<div class="col col--grow">
<h5>Total Obligated</h5>
<p>{{ task_order.total_obligated_funds | dollars }}</p>
</div>
<div class="col col--grow">
<h5>Total Expended</h5>
<p>{{ task_order.invoiced_funds | dollars }}</p>
</div>
</div>
{%- endif %}
</div> </div>
{% endfor %}
{% else %}
<div class="accordion__content--empty">
{{ "task_orders.status_empty_state" | translate({ 'status': status }) }}
</div> </div>
{% endfor %} {% endif %}
{% endcall %} {% endcall %}
</div> </div>
{% endmacro %} {% endmacro %}
@@ -62,8 +77,12 @@
<div class="portfolio-funding"> <div class="portfolio-funding">
{% if task_orders %} {% if to_count > 0 %}
{{ TaskOrderList(task_orders) }} {% call AccordionList() %}
{% for status, to_list in task_orders.items() %}
{{ TaskOrderList(to_list, status) }}
{% endfor %}
{% endcall %}
{% else %} {% else %}
{{ EmptyState( {{ EmptyState(
header="task_orders.empty_state.header"|translate, header="task_orders.empty_state.header"|translate,

View File

@@ -1,27 +1,81 @@
import pytest import pytest
from unittest.mock import Mock
from uuid import uuid4 from uuid import uuid4
from atst.domain.csp.cloud import AzureCloudProvider from atst.domain.csp.cloud import AzureCloudProvider
from tests.mock_azure import mock_azure, AUTH_CREDENTIALS from tests.mock_azure import mock_azure, AUTH_CREDENTIALS
from tests.factories import EnvironmentFactory from tests.factories import EnvironmentFactory, ApplicationFactory
def test_create_environment_succeeds(mock_azure: AzureCloudProvider): # TODO: Directly test create subscription, provide all args √
# TODO: Test create environment (create management group with parent)
# TODO: Test create application (create manageemnt group with parent)
# Create reusable mock for mocking the management group calls for multiple services
#
def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create() environment = EnvironmentFactory.create()
subscription_id = str(uuid4()) subscription_id = str(uuid4())
credentials = mock_azure._get_credential_obj(AUTH_CREDENTIALS)
display_name = "Test Subscription"
billing_profile_id = str(uuid4())
sku_id = str(uuid4())
management_group_id = (
environment.cloud_id # environment.csp_details.management_group_id?
)
billing_account_name = (
"?" # environment.application.portfilio.csp_details.billing_account.name?
)
invoice_section_name = "?" # environment.name? or something specific to billing?
mock_azure.sdk.subscription.SubscriptionClient.return_value.subscription_factory.create_subscription.return_value.result.return_value.subscription_link = ( mock_azure.sdk.subscription.SubscriptionClient.return_value.subscription_factory.create_subscription.return_value.result.return_value.subscription_link = (
f"subscriptions/{subscription_id}" f"subscriptions/{subscription_id}"
) )
result = mock_azure._create_subscription(
credentials,
display_name,
billing_profile_id,
sku_id,
management_group_id,
billing_account_name,
invoice_section_name,
)
assert result == subscription_id
def mock_management_group_create(mock_azure, spec_dict):
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = Mock(
**spec_dict
)
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"})
result = mock_azure.create_environment( result = mock_azure.create_environment(
AUTH_CREDENTIALS, environment.creator, environment AUTH_CREDENTIALS, environment.creator, environment
) )
assert result == subscription_id assert result.id == "Test Id"
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
application = ApplicationFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"})
result = mock_azure._create_application(AUTH_CREDENTIALS, application)
assert result.id == "Test Id"
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider): def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):

View File

@@ -3,78 +3,11 @@ from datetime import date, timedelta
from decimal import Decimal from decimal import Decimal
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders
from atst.models import Attachment, TaskOrder from atst.models import Attachment
from atst.models.task_order import TaskOrder, SORT_ORDERING, Status
from tests.factories import TaskOrderFactory, CLINFactory, PortfolioFactory from tests.factories import TaskOrderFactory, CLINFactory, PortfolioFactory
def test_task_order_sorting():
"""
Task orders should be listed first by status, and then by time_created.
"""
today = date.today()
yesterday = today - timedelta(days=1)
future = today + timedelta(days=100)
task_orders = [
# Draft
TaskOrderFactory.create(pdf=None),
TaskOrderFactory.create(pdf=None),
TaskOrderFactory.create(pdf=None),
# Active
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
),
# Upcoming
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=future, end_date=future)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=future, end_date=future)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=future, end_date=future)],
),
# Expired
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
# Unsigned
TaskOrderFactory.create(
clins=[CLINFactory.create(start_date=today, end_date=today)]
),
TaskOrderFactory.create(
clins=[CLINFactory.create(start_date=today, end_date=today)]
),
TaskOrderFactory.create(
clins=[CLINFactory.create(start_date=today, end_date=today)]
),
]
assert TaskOrders.sort(task_orders) == task_orders
def test_create_adds_clins(): def test_create_adds_clins():
portfolio = PortfolioFactory.create() portfolio = PortfolioFactory.create()
clins = [ clins = [
@@ -177,3 +110,47 @@ def test_delete_task_order_with_clins(session):
assert not session.query( assert not session.query(
session.query(TaskOrder).filter_by(id=task_order.id).exists() session.query(TaskOrder).filter_by(id=task_order.id).exists()
).scalar() ).scalar()
def test_task_order_sort_by_status():
today = date.today()
yesterday = today - timedelta(days=1)
future = today + timedelta(days=100)
initial_to_list = [
# Draft
TaskOrderFactory.create(pdf=None),
TaskOrderFactory.create(pdf=None),
TaskOrderFactory.create(pdf=None),
# Active
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
),
# Upcoming
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=future, end_date=future)],
),
# Expired
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
# Unsigned
TaskOrderFactory.create(
clins=[CLINFactory.create(start_date=today, end_date=today)]
),
]
sorted_by_status = TaskOrders.sort_by_status(initial_to_list)
assert len(sorted_by_status["Draft"]) == 3
assert len(sorted_by_status["Active"]) == 1
assert len(sorted_by_status["Upcoming"]) == 1
assert len(sorted_by_status["Expired"]) == 2
assert len(sorted_by_status["Unsigned"]) == 1
assert list(sorted_by_status.keys()) == [status.value for status in SORT_ORDERING]

View File

@@ -10,9 +10,9 @@ AZURE_CONFIG = {
} }
AUTH_CREDENTIALS = { AUTH_CREDENTIALS = {
"CLIENT_ID": AZURE_CONFIG["AZURE_CLIENT_ID"], "client_id": AZURE_CONFIG["AZURE_CLIENT_ID"],
"SECRET_KEY": AZURE_CONFIG["AZURE_SECRET_KEY"], "secret_key": AZURE_CONFIG["AZURE_SECRET_KEY"],
"TENANT_ID": AZURE_CONFIG["AZURE_TENANT_ID"], "tenant_id": AZURE_CONFIG["AZURE_TENANT_ID"],
} }
@@ -28,6 +28,12 @@ def mock_authorization():
return Mock(spec=authorization) return Mock(spec=authorization)
def mock_managementgroups():
from azure.mgmt import managementgroups
return Mock(spec=managementgroups)
def mock_graphrbac(): def mock_graphrbac():
import azure.graphrbac as graphrbac import azure.graphrbac as graphrbac
@@ -46,6 +52,7 @@ class MockAzureSDK(object):
self.subscription = mock_subscription() self.subscription = mock_subscription()
self.authorization = mock_authorization() self.authorization = mock_authorization()
self.managementgroups = mock_managementgroups()
self.graphrbac = mock_graphrbac() self.graphrbac = mock_graphrbac()
self.credentials = mock_credentials() self.credentials = mock_credentials()
# may change to a JEDI cloud # may change to a JEDI cloud

View File

@@ -12,6 +12,7 @@ from atst.domain.application_roles import ApplicationRoles
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.common import Paginator from atst.domain.common import Paginator
from atst.domain.csp.cloud import GeneralCSPException
from atst.domain.permission_sets import PermissionSets from atst.domain.permission_sets import PermissionSets
from atst.models.application_role import Status as ApplicationRoleStatus from atst.models.application_role import Status as ApplicationRoleStatus
from atst.models.environment_role import CSPRole, EnvironmentRole from atst.models.environment_role import CSPRole, EnvironmentRole
@@ -748,3 +749,41 @@ def test_handle_update_member(set_g):
assert len(application.roles) == 1 assert len(application.roles) == 1
assert len(app_role.environment_roles) == 1 assert len(app_role.environment_roles) == 1
assert app_role.environment_roles[0].environment == env assert app_role.environment_roles[0].environment == env
def test_handle_update_member_with_error(set_g, monkeypatch, mock_logger):
exception = "An error occurred."
def _raise_csp_exception(*args, **kwargs):
raise GeneralCSPException(exception)
monkeypatch.setattr(
"atst.domain.environments.Environments.update_env_role", _raise_csp_exception
)
user = UserFactory.create()
application = ApplicationFactory.create(
environments=[{"name": "Naboo"}, {"name": "Endor"}]
)
(env, env_1) = application.environments
app_role = ApplicationRoleFactory(application=application)
set_g("current_user", application.portfolio.owner)
set_g("portfolio", application.portfolio)
set_g("application", application)
form_data = ImmutableMultiDict(
{
"environment_roles-0-environment_id": env.id,
"environment_roles-0-role": "Basic Access",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True,
"perms_team_mgmt": True,
"perms_del_env": True,
}
)
handle_update_member(application.id, app_role.id, form_data)
assert mock_logger.messages[-1] == exception

View File

@@ -29,8 +29,10 @@ def task_order():
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user) portfolio = PortfolioFactory.create(owner=user)
attachment = Attachment(filename="sample_attachment", object_name="sample") attachment = Attachment(filename="sample_attachment", object_name="sample")
task_order = TaskOrderFactory.create(portfolio=portfolio)
CLINFactory.create(task_order=task_order)
return TaskOrderFactory.create(portfolio=portfolio) return task_order
def test_review_task_order_not_draft(client, user_session, task_order): def test_review_task_order_not_draft(client, user_session, task_order):

View File

@@ -19,6 +19,16 @@ def build_pdf_form_data(filename="sample.pdf", object_name=None):
def task_order(): def task_order():
user = UserFactory.create() user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user) portfolio = PortfolioFactory.create(owner=user)
task_order = TaskOrderFactory.create(portfolio=portfolio)
CLINFactory.create(task_order=task_order)
return task_order
@pytest.fixture
def incomplete_to():
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
return TaskOrderFactory.create(portfolio=portfolio) return TaskOrderFactory.create(portfolio=portfolio)
@@ -234,7 +244,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to(
}, },
] ]
TaskOrders.create_clins(task_order.id, clin_list) TaskOrders.create_clins(task_order.id, clin_list)
assert len(task_order.clins) == 2 assert len(task_order.clins) == 3
user_session(task_order.portfolio.owner) user_session(task_order.portfolio.owner)
form_data = { form_data = {
@@ -267,11 +277,11 @@ def test_task_orders_form_step_four_review(client, user_session, completed_task_
def test_task_orders_form_step_four_review_incomplete_to( def test_task_orders_form_step_four_review_incomplete_to(
client, user_session, task_order client, user_session, incomplete_to
): ):
user_session(task_order.portfolio.owner) user_session(incomplete_to.portfolio.owner)
response = client.get( response = client.get(
url_for("task_orders.form_step_four_review", task_order_id=task_order.id) url_for("task_orders.form_step_four_review", task_order_id=incomplete_to.id)
) )
assert response.status_code == 404 assert response.status_code == 404
@@ -290,12 +300,13 @@ def test_task_orders_form_step_five_confirm_signature(
def test_task_orders_form_step_five_confirm_signature_incomplete_to( def test_task_orders_form_step_five_confirm_signature_incomplete_to(
client, user_session, task_order client, user_session, incomplete_to
): ):
user_session(task_order.portfolio.owner) user_session(incomplete_to.portfolio.owner)
response = client.get( response = client.get(
url_for( url_for(
"task_orders.form_step_five_confirm_signature", task_order_id=task_order.id "task_orders.form_step_five_confirm_signature",
task_order_id=incomplete_to.id,
) )
) )
assert response.status_code == 404 assert response.status_code == 404

View File

@@ -40,6 +40,9 @@ class FakeLogger:
def error(self, msg, *args, **kwargs): def error(self, msg, *args, **kwargs):
self._log("error", msg, *args, **kwargs) self._log("error", msg, *args, **kwargs)
def exception(self, msg, *args, **kwargs):
self._log("exception", msg, *args, **kwargs)
def _log(self, _lvl, msg, *args, **kwargs): def _log(self, _lvl, msg, *args, **kwargs):
self.messages.append(msg) self.messages.append(msg)
if "extra" in kwargs: if "extra" in kwargs:

View File

@@ -529,6 +529,8 @@ task_orders:
team_title: Your team team_title: Your team
sign: sign:
digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature. digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature.
status_empty_state: 'This Portfolio has no {status} Task Orders.'
status_list_title: '{status} Task Orders'
JEDICLINType: JEDICLINType:
JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS' JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS'
JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS' JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS'