diff --git a/Pipfile b/Pipfile index 68c97392..8b0383fb 100644 --- a/Pipfile +++ b/Pipfile @@ -25,6 +25,7 @@ PyYAML = "*" azure-storage = "*" azure-storage-common = "*" celery = "*" +azure-mgmt-subscription = "*" [dev-packages] bandit = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 3cf4e198..aafa0dbb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,21 +1,26 @@ { "_meta": { "hash": { - "sha256": "2f366c5a5f62ba5a451ca024759cc8cbf481a15e5198d73311fa9cf194d7f547" + "sha256": "eca6e06b6a34bdba3872c595957d840da21f3e58cb41f3ec02b7c3a424a5e4c4" }, "pipfile-spec": 6, "requires": { "python_version": "3.7.3" }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] + "sources": [{ + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + }] }, "default": { + "adal": { + "hashes": [ + "sha256:5a7f1e037c6290c6d7609cab33a9e5e988c2fbec5c51d1c4c649ee3faff37eaf", + "sha256:fd17e5661f60634ddf96a569b95d34ccb8a98de60593d729c28bdcfe360eaad1" + ], + "version": "==1.2.2" + }, "alembic": { "hashes": [ "sha256:9f907d7e8b286a1cfb22db9084f9ce4fde7ad7956bb496dc7c952e10ac90e36a" @@ -45,6 +50,14 @@ ], "version": "==1.1.23" }, + "azure-mgmt-subscription": { + "hashes": [ + "sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1", + "sha256:850f86de5078f61f3a9bd81cb2b938ea25ce9a266c9fe22c7fca94a2b5968c16" + ], + "index": "pypi", + "version": "==0.5.0" + }, "azure-nspkg": { "hashes": [ "sha256:1d0bbb2157cf57b1bef6c8c8e5b41133957364456c43b0a43599890023cca0a8", @@ -221,6 +234,13 @@ ], "version": "==0.23" }, + "isodate": { + "hashes": [ + "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", + "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81" + ], + "version": "==0.6.0" + }, "itsdangerous": { "hashes": [ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", @@ -296,6 +316,27 @@ ], "version": "==7.2.0" }, + "msrest": { + "hashes": [ + "sha256:56b8b5b4556fb2a92cac640df267d560889bdc9e2921187772d4691d97bc4e8d", + "sha256:f5153bfe60ee757725816aedaa0772cbfe0bddb52cd2d6db4cb8b4c3c6c6f928" + ], + "version": "==0.6.10" + }, + "msrestazure": { + "hashes": [ + "sha256:63db9f646fffc9244b332090e679d1e5f283ac491ee0cc321f5116f9450deb4a", + "sha256:fecb6a72a3eb5483e4deff38210d26ae42d3f6d488a7a275bd2423a1a014b22c" + ], + "version": "==0.6.2" + }, + "oauthlib": { + "hashes": [ + "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", + "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" + ], + "version": "==3.1.0" + }, "pendulum": { "hashes": [ "sha256:1cde6e3c6310fb882c98f373795f807cb2bd6af01f34d2857e6e283b5ee91e09", @@ -350,6 +391,13 @@ ], "version": "==2.19" }, + "pyjwt": { + "hashes": [ + "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", + "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + ], + "version": "==1.7.1" + }, "pyopenssl": { "hashes": [ "sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200", diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index 7bcbf44f..58dac63a 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -383,3 +383,126 @@ class MockCloudProvider(CloudProviderInterface): self._delay(1, 5) if self._with_authorization and credentials != self._auth_credentials: raise self.AUTHENTICATION_EXCEPTION + + +AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD +AZURE_SKU_ID = "?" # probably a static sku specific to ATAT/JEDI +SUBSCRIPTION_ID_REGEX = re.compile( + "subscriptions\/([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})", + re.I, +) + +class AzureCloudProvider(CloudProviderInterface): + def __init__(self, config): + self.config = config + + self.client_id = config["AZURE_CLIENT_ID"] + self.secret_key = config["AZURE_SECRET_KEY"] + self.tenant_id = config["AZURE_TENANT_ID"] + + import azure.mgmt as mgmt + import azure.graphrbac as graphrbac + import azure.common.credentials as credentials + + self.azure_mgmt = mgmt + self.azure_graph = graphrbac + self.azure_credentials = credentials + + def root_creds(self): + return { + "client_id": self.client_id, + "secret_key": self.secret_key, + "tenant_id": self.tenant_id, + } + + def create_environment( + self, auth_credentials: Dict, user: User, environment: Environment + ): + credentials = self._get_credential_obj(self.root_creds()) + sub_client = self.azure_mgmt.subscription.SubscriptionClient(credentials) + + display_name = ( + f"{environment.application.name}_{environment.name}_{environment.id}" + ) # proposed format + + billing_profile_id = "?" # something chained from environment? + sku_id = AZURE_SKU_ID + body = self.azure_mgmt.subscription.models.ModernSubscriptionCreationParameters( + display_name, billing_profile_id, sku_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 = "?" + 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( + self, auth_credentials: Dict, csp_environment_id: str + ) -> Dict: + root_creds = self.root_creds() + credentials = self._get_credential_obj(root_creds) + + self.azure_mgmt. + + sub_client = self.azure_mgmt.subscription.SubscriptionClient(credentials) + subscription: self.azure_mgmt.subscription.models.Subscription = sub_client.subscriptions.get( + csp_environment_id + ) + + # how do we scope the graph client to the new subscription rather than + # the cloud0 subscription? tenant id seems to be separate from subscription id + graph_client = self.azure_graph.GraphRbacManagementClient( + credentials, root_creds.get("tenant_id") + app_create_param = self.azure_graph.models.ApplicationCreateParameters( + display_name=app_display_name + ) + app: self.azure_graph.models.Application = graph_client.applications.create( + app_create_param + ) + + self.azure_graph.models. + + # create a new service principle for the new application, which should be scoped + # to the new subscription + app_id = app.app_id + sp_create_params = self.azure_graph.models.ServicePrincipalCreateParameters( + app_id=app_id, account_enabled=True + ) + + service_principal = graph_client.service_principals.create(sp_create_params) + + return { + "csp_user_id": service_principal.object_id, + "credentials": service_principal.password_credentials, + } + + def _extract_subscription_id(self, subscription_url): + sub_id_match = SUBSCRIPTION_ID_REGEX.match(subscription_url) + + if sub_id_match: + return sub_id_match.group(1) + + def _get_credential_obj(self, creds, resource="https://graph.windows.net"): + return self.azure_credentials.ServicePrincipalCredentials( + client_id=creds.get("client_id"), + secret=creds.get("secret_key"), + tenant=creds.get("tenant_id"), + resource=resource, + cloud_environment=AZURE_ENVIRONMENT, + ) +