diff --git a/.secrets.baseline b/.secrets.baseline index 385c0b04..05921baa 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "lines": null }, - "generated_at": "2019-12-05T17:54:05Z", + "generated_at": "2019-12-06T21:22:07Z", "plugins_used": [ { "base64_limit": 4.5, @@ -161,7 +161,7 @@ "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "is_secret": false, "is_verified": false, - "line_number": 31, + "line_number": 41, "type": "Hex High Entropy String" } ], diff --git a/Pipfile b/Pipfile index 6456afb1..edc8bbfc 100644 --- a/Pipfile +++ b/Pipfile @@ -29,6 +29,7 @@ azure-mgmt-subscription = "*" azure-graphrbac = "*" msrestazure = "*" azure-mgmt-authorization = "*" +azure-mgmt-managementgroups = "*" [dev-packages] bandit = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 9c5233bb..80f6fc96 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6d2ab855267daac877ae7464de9dba5b62b7d89288992f87d8fc6ff0c0d2520f" + "sha256": "c2b19c436646705ea3bf4df8c35c2833083f048da37fc619e66f7236153607c5" }, "pipfile-spec": 6, "requires": { @@ -39,11 +39,11 @@ }, "apache-libcloud": { "hashes": [ - "sha256:201751f738109f25d58dcdfb5804e17216e0dc8f68b522e9e26ac16e0b9ff2ea", - "sha256:40215db1bd489d17dc1abfdb289d7f035313c7297b6a7462c79d8287cbbeae91" + "sha256:9bc5cd5c32151bb7a04a7c7de0be9b4a4b8271e348ac91dd79eaaeeae627115f", + "sha256:fcc165f2cc2db9a379c6d3a17b3beb9081bb64ba5c0bf7bbb58da864810092f0" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.6.1" }, "azure-common": { "hashes": [ @@ -68,6 +68,14 @@ "index": "pypi", "version": "==0.60.0" }, + "azure-mgmt-managementgroups": { + "hashes": [ + "sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682", + "sha256:8194ee6274df865eccd1ed9d385ea625aeba9b8058b9e4fdf547f5207271a775" + ], + "index": "pypi", + "version": "==0.2.0" + }, "azure-mgmt-subscription": { "hashes": [ "sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1", @@ -117,10 +125,10 @@ }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "cffi": { "hashes": [ @@ -248,10 +256,10 @@ }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21", + "sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742" ], - "version": "==0.23" + "version": "==1.1.0" }, "isodate": { "hashes": [ @@ -330,10 +338,10 @@ }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", + "sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45" ], - "version": "==7.2.0" + "version": "==8.0.0" }, "msrest": { "hashes": [ @@ -422,11 +430,11 @@ }, "pyopenssl": { "hashes": [ - "sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200", - "sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6" + "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504", + "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507" ], "index": "pypi", - "version": "==19.0.0" + "version": "==19.1.0" }, "python-dateutil": { "hashes": [ @@ -459,22 +467,20 @@ }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2" }, "redis": { "hashes": [ @@ -650,10 +656,10 @@ }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "chardet": { "hashes": [ @@ -785,10 +791,10 @@ }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21", + "sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742" ], - "version": "==0.23" + "version": "==1.1.0" }, "ipdb": { "hashes": [ @@ -799,11 +805,11 @@ }, "ipython": { "hashes": [ - "sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280", - "sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995" + "sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51", + "sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e" ], "index": "pypi", - "version": "==7.9.0" + "version": "==7.10.1" }, "ipython-genutils": { "hashes": [ @@ -908,30 +914,30 @@ }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", + "sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45" ], - "version": "==7.2.0" + "version": "==8.0.0" }, "mypy": { "hashes": [ - "sha256:1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f", - "sha256:31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00", - "sha256:3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae", - "sha256:48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d", - "sha256:540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9", - "sha256:672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391", - "sha256:6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f", - "sha256:9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9", - "sha256:ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990", - "sha256:b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7", - "sha256:d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb", - "sha256:d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453", - "sha256:dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b", - "sha256:f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb" + "sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768", + "sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646", + "sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c", + "sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939", + "sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279", + "sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2", + "sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2", + "sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640", + "sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7", + "sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c", + "sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17", + "sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78", + "sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931", + "sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a" ], "index": "pypi", - "version": "==0.740" + "version": "==0.750" }, "mypy-extensions": { "hashes": [ @@ -961,10 +967,10 @@ }, "pbr": { "hashes": [ - "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", - "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" + "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", + "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488" ], - "version": "==5.4.3" + "version": "==5.4.4" }, "pexpect": { "hashes": [ @@ -983,18 +989,17 @@ }, "pluggy": { "hashes": [ - "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", - "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==0.13.0" + "version": "==0.13.1" }, "prompt-toolkit": { "hashes": [ - "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", - "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", - "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db" + "sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7", + "sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990" ], - "version": "==2.0.10" + "version": "==3.0.2" }, "ptyprocess": { "hashes": [ @@ -1012,10 +1017,10 @@ }, "pygments": { "hashes": [ - "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", - "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", + "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" ], - "version": "==2.4.2" + "version": "==2.5.2" }, "pylint": { "hashes": [ @@ -1058,11 +1063,11 @@ }, "pytest-mock": { "hashes": [ - "sha256:b3514caac35fe3f05555923eabd9546abce11571cc2ddf7d8615959d04f2c89e", - "sha256:ea502c3891599c26243a3a847ccf0b1d20556678c528f86c98e3cd6d40c5cf11" + "sha256:96a0cebc66e09930be2a15b03333d90b59584d3fb011924f81c14b50ee0afbba", + "sha256:e5381be2608e49547f5e47633c5f81241ebf6206d17ce516a7a18d5a917e3859" ], "index": "pypi", - "version": "==1.11.2" + "version": "==1.12.1" }, "pytest-watch": { "hashes": [ @@ -1080,22 +1085,20 @@ }, "pyyaml": { "hashes": [ - "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", - "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", - "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", - "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", - "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", - "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", - "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", - "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", - "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", - "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", - "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", - "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", - "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", + "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", + "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", + "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", + "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", + "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", + "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", + "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", + "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", + "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", + "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" ], "index": "pypi", - "version": "==5.1.2" + "version": "==5.2" }, "regex": { "hashes": [ diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index 4cd69d3c..2da48642 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -3,6 +3,7 @@ import re from uuid import uuid4 from atst.models.user import User +from atst.models.application import Application from atst.models.environment import Environment from atst.models.environment_role import EnvironmentRole @@ -399,13 +400,14 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00 class AzureSDKProvider(object): def __init__(self): - from azure.mgmt import subscription, authorization + from azure.mgmt import subscription, authorization, managementgroups import azure.graphrbac as graphrbac import azure.common.credentials as credentials from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD self.subscription = subscription self.authorization = authorization + self.managementgroups = managementgroups self.graphrbac = graphrbac self.credentials = credentials # may change to a JEDI cloud @@ -428,42 +430,23 @@ class AzureCloudProvider(CloudProviderInterface): def create_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) - sub_client = self.sdk.subscription.SubscriptionClient(credentials) - 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? - sku_id = AZURE_SKU_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= + management_group = self._create_management_group( + credentials, management_group_id, display_name, parent_id, ) - # These 2 seem like something that might be worthwhile to allow tiebacks to - # 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 + return management_group def create_atat_admin_user( self, auth_credentials: Dict, csp_environment_id: str @@ -502,6 +485,83 @@ class AzureCloudProvider(CloudProviderInterface): "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): # we really should be using graph.microsoft.com, but i'm getting # "expired token" errors for that diff --git a/atst/domain/task_orders.py b/atst/domain/task_orders.py index 0c67e1d4..24acdee7 100644 --- a/atst/domain/task_orders.py +++ b/atst/domain/task_orders.py @@ -64,10 +64,12 @@ class TaskOrders(BaseDomainClass): db.session.commit() @classmethod - def sort(cls, task_orders: [TaskOrder]) -> [TaskOrder]: - # Sorts a list of task orders on two keys: status (primary) and time_created (secondary) - 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)) + def sort_by_status(cls, task_orders): + by_status = {status.value: [] for status in SORT_ORDERING} + + for task_order in task_orders: + by_status[task_order.display_status].append(task_order) + return by_status @classmethod diff --git a/atst/models/task_order.py b/atst/models/task_order.py index 85bf363a..1a46e505 100644 --- a/atst/models/task_order.py +++ b/atst/models/task_order.py @@ -1,5 +1,5 @@ -from datetime import timedelta from enum import Enum +from decimal import Decimal from sqlalchemy import Column, DateTime, ForeignKey, String from sqlalchemy.ext.hybrid import hybrid_property @@ -17,15 +17,16 @@ class Status(Enum): ACTIVE = "Active" UPCOMING = "Upcoming" EXPIRED = "Expired" - UNSIGNED = "Not signed" + UNSIGNED = "Unsigned" -SORT_ORDERING = { - status: order - for (order, status) in enumerate( - [Status.DRAFT, Status.ACTIVE, Status.UPCOMING, Status.EXPIRED, Status.UNSIGNED] - ) -} +SORT_ORDERING = [ + Status.ACTIVE, + Status.DRAFT, + Status.UPCOMING, + Status.EXPIRED, + Status.UNSIGNED, +] class TaskOrder(Base, mixins.TimestampsMixin): @@ -131,12 +132,11 @@ class TaskOrder(Base, mixins.TimestampsMixin): @property 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 def end_date(self): - default_end_date = self.start_date + timedelta(days=1) - return max((c.end_date for c in self.clins), default=default_end_date) + return max((c.end_date for c in self.clins), default=None) @property def days_to_expiration(self): @@ -170,6 +170,11 @@ class TaskOrder(Base, mixins.TimestampsMixin): # Faked for display purposes 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 def display_status(self): return self.status.value diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index a4aea550..f2d252a9 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -20,6 +20,7 @@ from atst.domain.permission_sets import PermissionSets from atst.utils.flash import formatted_flash as flash from atst.utils.localization import translate from atst.jobs import send_mail +from atst.routes.errors import log_error 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) - except GeneralCSPException: + except GeneralCSPException as exc: + log_error(exc) flash( "application_member_update_error", user_name=app_role.user_name, ) diff --git a/atst/routes/task_orders/index.py b/atst/routes/task_orders/index.py index a58ea8da..e02780ca 100644 --- a/atst/routes/task_orders/index.py +++ b/atst/routes/task_orders/index.py @@ -6,7 +6,6 @@ from atst.domain.portfolios import Portfolios from atst.domain.task_orders import TaskOrders from atst.forms.task_order import SignatureForm from atst.models import Permissions -from atst.models.task_order import Status as TaskOrderStatus @task_orders_bp.route("/task_orders//review") @@ -28,14 +27,9 @@ def review_task_order(task_order_id): @user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding") def portfolio_funding(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) - task_orders = TaskOrders.sort(portfolio.task_orders) - label_colors = { - TaskOrderStatus.DRAFT: "warning", - TaskOrderStatus.ACTIVE: "success", - TaskOrderStatus.UPCOMING: "info", - TaskOrderStatus.EXPIRED: "error", - TaskOrderStatus.UNSIGNED: "purple", - } + task_orders = TaskOrders.sort_by_status(portfolio.task_orders) + to_count = len(portfolio.task_orders) + # TODO: Get expended amount from the CSP 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 ) diff --git a/deploy/azure/atst-envvars-configmap.yml b/deploy/azure/atst-envvars-configmap.yml index 8907493d..d2b0ba45 100644 --- a/deploy/azure/atst-envvars-configmap.yml +++ b/deploy/azure/atst-envvars-configmap.yml @@ -13,10 +13,10 @@ data: CDN_ORIGIN: https://azure.atat.code.mil CELERY_DEFAULT_QUEUE: celery-master CSP: azure - DEBUG: 0 + DEBUG: "0" FLASK_ENV: master LOG_JSON: "true" - MAIL_PORT: 587 + MAIL_PORT: "587" MAIL_SENDER: postmaster@atat.code.mil MAIL_SERVER: smtp.mailgun.org MAIL_TLS: "true" @@ -24,7 +24,7 @@ data: PGAPPNAME: atst PGDATABASE: staging PGHOST: atat-db.postgres.database.azure.com - PGPORT: 5432 + PGPORT: "5432" PGSSLMODE: verify-full PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt PGUSER: atat_master@atat-db diff --git a/deploy/azure/atst-worker-envvars-configmap.yml b/deploy/azure/atst-worker-envvars-configmap.yml index ab10c118..4323952c 100644 --- a/deploy/azure/atst-worker-envvars-configmap.yml +++ b/deploy/azure/atst-worker-envvars-configmap.yml @@ -9,9 +9,9 @@ data: AZURE_TO_BUCKET_NAME: task-order-pdfs CAC_URL: https://auth-staging.atat.code.mil/login-redirect CELERY_DEFAULT_QUEUE: celery-master - DEBUG: 0 + DEBUG: "0" DISABLE_CRL_CHECK: "true" - MAIL_PORT: 587 + MAIL_PORT: "587" MAIL_SENDER: postmaster@atat.code.mil MAIL_SERVER: smtp.mailgun.org MAIL_TLS: "true" @@ -19,7 +19,7 @@ data: PGAPPNAME: atst PGDATABASE: staging PGHOST: atat-db.postgres.database.azure.com - PGPORT: 5432 + PGPORT: "5432" PGSSLMODE: verify-full PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt PGUSER: atat_master@atat-db diff --git a/js/components/accordion.js b/js/components/accordion.js index d281a9e7..b96e1b80 100644 --- a/js/components/accordion.js +++ b/js/components/accordion.js @@ -11,4 +11,10 @@ export default { default: false, }, }, + + methods: { + collapse: function() { + this.isVisible = false + }, + }, } diff --git a/js/components/accordion_list.js b/js/components/accordion_list.js new file mode 100644 index 00000000..41e11d33 --- /dev/null +++ b/js/components/accordion_list.js @@ -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()) + }, + }, +} diff --git a/js/index.js b/js/index.js index a28c4868..fb5cdd6e 100644 --- a/js/index.js +++ b/js/index.js @@ -7,6 +7,8 @@ import Vue from 'vue/dist/vue' import VTooltip from 'v-tooltip' import stickybits from 'stickybits' +import Accordion from './components/accordion' +import AccordionList from './components/accordion_list' import dodlogin from './components/dodlogin' import optionsinput from './components/options_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 ClinFields from './components/clin_fields' import PopDateRange from './components/pop_date_range' -import Accordion from './components/accordion' import ToggleMenu from './components/toggle_menu' Vue.config.productionTip = false @@ -42,6 +43,7 @@ const app = new Vue({ el: '#app-root', components: { Accordion, + AccordionList, dodlogin, toggler, optionsinput, diff --git a/js/mixins/toggle.js b/js/mixins/toggle.js index d891eb02..3e155dd3 100644 --- a/js/mixins/toggle.js +++ b/js/mixins/toggle.js @@ -17,6 +17,7 @@ export default { methods: { toggle: function(e) { e.preventDefault() + e.stopPropagation() this.isVisible = !this.isVisible }, }, diff --git a/styles/elements/_accordions.scss b/styles/elements/_accordions.scss index a6e61692..8d21ef62 100644 --- a/styles/elements/_accordions.scss +++ b/styles/elements/_accordions.scss @@ -46,5 +46,20 @@ 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; + } } } diff --git a/styles/sections/_task_order.scss b/styles/sections/_task_order.scss index 4307333a..228bf126 100644 --- a/styles/sections/_task_order.scss +++ b/styles/sections/_task_order.scss @@ -127,21 +127,6 @@ width: 100%; } - .label { - &--pending, - &--started { - background-color: $color-gold; - } - - &--active { - background-color: $color-green; - } - - &--expired { - background-color: $color-red; - } - } - .task-order-document-link { &__icon { padding-top: 0.5rem; diff --git a/templates/applications/index.html b/templates/applications/index.html index 81509be5..00a9b0e8 100644 --- a/templates/applications/index.html +++ b/templates/applications/index.html @@ -1,4 +1,5 @@ {% from "components/accordion.html" import Accordion %} +{% from "components/accordion_list.html" import AccordionList %} {% from "components/empty_state.html" import EmptyState %} {% from "components/sticky_cta.html" import StickyCTA %} {% from "components/icon.html" import Icon %} @@ -32,7 +33,7 @@ ) }} {% else %} -
+ {% call AccordionList() %} {% for application in portfolio.applications|sort(attribute='name') %} {% set section_name = "application-{}".format(application.id) %} {% set title = "Environments ({})".format(application.environments|length) %} @@ -76,7 +77,7 @@ {% endcall %}
{% endfor %} - + {% endcall %} {% endif %} diff --git a/templates/components/accordion_list.html b/templates/components/accordion_list.html new file mode 100644 index 00000000..216e51b1 --- /dev/null +++ b/templates/components/accordion_list.html @@ -0,0 +1,11 @@ +{% macro AccordionList() %} + +
+ + + {{ caller() }} +
+
+{% endmacro %} diff --git a/templates/task_orders/index.html b/templates/task_orders/index.html index 0c319403..b9277123 100644 --- a/templates/task_orders/index.html +++ b/templates/task_orders/index.html @@ -1,4 +1,5 @@ {% from "components/accordion.html" import Accordion %} +{% from "components/accordion_list.html" import AccordionList %} {% from "components/empty_state.html" import EmptyState %} {% from "components/icon.html" import Icon %} {% from "components/sticky_cta.html" import StickyCTA %} @@ -13,38 +14,52 @@ {% macro TaskOrderList(task_orders, status) %} - {% set status = "All Task Orders" %} -
- {% call Accordion(title=status, id=status, heading_tag="h4") %} - {% for task_order in task_orders %} -
-

Task Order #{{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}

-
-
-
- Current Period of Performance -
-

- {{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }} - - - {{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }} -

-
-
-
Total Value
-

{{ task_order.total_contract_amount | dollars }}

-
-
-
Total Obligated
-

{{ task_order.total_obligated_funds | dollars }}

-
-
-
Total Expended
-

$0

-
+
+ {% call Accordion(title=("task_orders.status_list_title"|translate({'status': status})), id=status, heading_tag="h4") %} + {% if task_orders|length > 0 %} + {% for task_order in task_orders %} + {% set to_number %} + {% if task_order.number != "" %} + Task Order #{{ task_order.number }} + {% else %} + New Task Order + {% endif %} + {% endset %} +
+

{{ to_number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}

+ {% if status != 'Expired' -%} +
+
+
+ Current Period of Performance +
+

+ {{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }} + - + {{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }} +

+
+
+
Total Value
+

{{ task_order.total_contract_amount | dollars }}

+
+
+
Total Obligated
+

{{ task_order.total_obligated_funds | dollars }}

+
+
+
Total Expended
+

{{ task_order.invoiced_funds | dollars }}

+
+
+ {%- endif %}
+ {% endfor %} + {% else %} +
+ {{ "task_orders.status_empty_state" | translate({ 'status': status }) }}
- {% endfor %} + {% endif %} {% endcall %}
{% endmacro %} @@ -62,8 +77,12 @@
- {% if task_orders %} - {{ TaskOrderList(task_orders) }} + {% if to_count > 0 %} + {% call AccordionList() %} + {% for status, to_list in task_orders.items() %} + {{ TaskOrderList(to_list, status) }} + {% endfor %} + {% endcall %} {% else %} {{ EmptyState( header="task_orders.empty_state.header"|translate, diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py index 19ad63c8..39c6655e 100644 --- a/tests/domain/cloud/test_azure_csp.py +++ b/tests/domain/cloud/test_azure_csp.py @@ -1,27 +1,81 @@ import pytest +from unittest.mock import Mock from uuid import uuid4 from atst.domain.csp.cloud import AzureCloudProvider 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() 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 = ( 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( 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): diff --git a/tests/domain/test_task_orders.py b/tests/domain/test_task_orders.py index 8b1eb724..7bc4cf41 100644 --- a/tests/domain/test_task_orders.py +++ b/tests/domain/test_task_orders.py @@ -3,78 +3,11 @@ from datetime import date, timedelta from decimal import Decimal 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 -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(): portfolio = PortfolioFactory.create() clins = [ @@ -177,3 +110,47 @@ def test_delete_task_order_with_clins(session): assert not session.query( session.query(TaskOrder).filter_by(id=task_order.id).exists() ).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] diff --git a/tests/mock_azure.py b/tests/mock_azure.py index 34d0aa53..a360df64 100644 --- a/tests/mock_azure.py +++ b/tests/mock_azure.py @@ -10,9 +10,9 @@ AZURE_CONFIG = { } AUTH_CREDENTIALS = { - "CLIENT_ID": AZURE_CONFIG["AZURE_CLIENT_ID"], - "SECRET_KEY": AZURE_CONFIG["AZURE_SECRET_KEY"], - "TENANT_ID": AZURE_CONFIG["AZURE_TENANT_ID"], + "client_id": AZURE_CONFIG["AZURE_CLIENT_ID"], + "secret_key": AZURE_CONFIG["AZURE_SECRET_KEY"], + "tenant_id": AZURE_CONFIG["AZURE_TENANT_ID"], } @@ -28,6 +28,12 @@ def mock_authorization(): return Mock(spec=authorization) +def mock_managementgroups(): + from azure.mgmt import managementgroups + + return Mock(spec=managementgroups) + + def mock_graphrbac(): import azure.graphrbac as graphrbac @@ -46,6 +52,7 @@ class MockAzureSDK(object): self.subscription = mock_subscription() self.authorization = mock_authorization() + self.managementgroups = mock_managementgroups() self.graphrbac = mock_graphrbac() self.credentials = mock_credentials() # may change to a JEDI cloud diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index 4961065e..08c979ad 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -12,6 +12,7 @@ from atst.domain.application_roles import ApplicationRoles from atst.domain.environment_roles import EnvironmentRoles from atst.domain.invitations import ApplicationInvitations from atst.domain.common import Paginator +from atst.domain.csp.cloud import GeneralCSPException from atst.domain.permission_sets import PermissionSets from atst.models.application_role import Status as ApplicationRoleStatus 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(app_role.environment_roles) == 1 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 diff --git a/tests/routes/task_orders/test_index.py b/tests/routes/task_orders/test_index.py index 708b2bdc..48d8bd6b 100644 --- a/tests/routes/task_orders/test_index.py +++ b/tests/routes/task_orders/test_index.py @@ -29,8 +29,10 @@ def task_order(): user = UserFactory.create() portfolio = PortfolioFactory.create(owner=user) 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): diff --git a/tests/routes/task_orders/test_new.py b/tests/routes/task_orders/test_new.py index 4d2f5fdb..1e8e16eb 100644 --- a/tests/routes/task_orders/test_new.py +++ b/tests/routes/task_orders/test_new.py @@ -19,6 +19,16 @@ def build_pdf_form_data(filename="sample.pdf", object_name=None): def task_order(): user = UserFactory.create() 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) @@ -234,7 +244,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to( }, ] 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) 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( - 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( - 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 @@ -290,12 +300,13 @@ def test_task_orders_form_step_five_confirm_signature( 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( 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 diff --git a/tests/utils.py b/tests/utils.py index 152c347a..66bf2b18 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -40,6 +40,9 @@ class FakeLogger: def error(self, 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): self.messages.append(msg) if "extra" in kwargs: diff --git a/translations.yaml b/translations.yaml index d63b1a5e..cb6b5376 100644 --- a/translations.yaml +++ b/translations.yaml @@ -529,6 +529,8 @@ task_orders: team_title: Your team sign: 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: JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS' JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS'