From 2801e074540b35eda1e28cc601667c58e8771fed Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 9 Dec 2019 13:58:24 -0500 Subject: [PATCH 1/4] Add Azure Management Group Dependency --- Pipfile | 1 + Pipfile.lock | 177 ++++++++++++++++++++++++++------------------------- 2 files changed, 91 insertions(+), 87 deletions(-) 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": [ From 8a1ed5b1936dd603d5c887b590012271a9568eaa Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 9 Dec 2019 14:00:36 -0500 Subject: [PATCH 2/4] Sketch in Management Group integration for Azure Add mocks and real implementations for creating nested management groups that reflect the Portfolio->Application->Environment->Subscription hierarchy. --- atst/domain/csp/cloud.py | 124 ++++++++++++++++++++------- tests/domain/cloud/test_azure_csp.py | 60 ++++++++++++- tests/mock_azure.py | 13 ++- 3 files changed, 159 insertions(+), 38 deletions(-) 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/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/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 From 8f94d9e6ec5cb68d3f1dcc94b3b06d95c6e9be94 Mon Sep 17 00:00:00 2001 From: dandds Date: Fri, 13 Dec 2019 06:20:38 -0500 Subject: [PATCH 3/4] Log any CSP errors that occur when disabling a user. When one user disables another's environment role in Azure, sometimes an exception will be raised. Since we catch the exception and display an error message to the user, we should also log the exception so that the error is traceable later. --- atst/routes/applications/settings.py | 4 ++- tests/routes/applications/test_settings.py | 39 ++++++++++++++++++++++ tests/utils.py | 3 ++ 3 files changed, 45 insertions(+), 1 deletion(-) 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/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/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: From 1466a302b24d0aa47a0d91c7aff6bdf40ae285b3 Mon Sep 17 00:00:00 2001 From: dandds Date: Wed, 11 Dec 2019 05:41:54 -0500 Subject: [PATCH 4/4] K8s YAML integer values need to be quoted. --- deploy/azure/atst-envvars-configmap.yml | 6 +++--- deploy/azure/atst-worker-envvars-configmap.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) 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