From 2ac333e0b738d5fa5ebc94e988a53ab40327cfdb Mon Sep 17 00:00:00 2001 From: tomdds Date: Thu, 9 Jan 2020 17:29:34 -0500 Subject: [PATCH] Sample create tenant itegration This integration works with the happy path, we'll need to expand some fields and handle error states more coherently. --- Pipfile | 2 + Pipfile.lock | 11 +++- atst/domain/csp/cloud.py | 76 ++++++++++++++++++++++++---- tests/domain/cloud/test_azure_csp.py | 21 +++++++- tests/mock_azure.py | 14 +++++ 5 files changed, 111 insertions(+), 13 deletions(-) diff --git a/Pipfile b/Pipfile index 0de14fa2..f1d852b9 100644 --- a/Pipfile +++ b/Pipfile @@ -33,6 +33,8 @@ azure-mgmt-authorization = "*" azure-mgmt-managementgroups = "*" azure-mgmt-resource = "*" transitions = "*" +azure-mgmt-consumption = "*" +adal = "*" [dev-packages] bandit = "*" diff --git a/Pipfile.lock b/Pipfile.lock index ef6d0203..681fddb1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "63b8f9d203f306a6f0ff20514b024909aa7e64917e1befcc9ea79931b5b4bd34" + "sha256": "a127b88e6c64842786f1868cb93bb1cdc828aa78040ea8ba4079bb3de0316dab" }, "pipfile-spec": 6, "requires": { @@ -21,6 +21,7 @@ "sha256:5a7f1e037c6290c6d7609cab33a9e5e988c2fbec5c51d1c4c649ee3faff37eaf", "sha256:fd17e5661f60634ddf96a569b95d34ccb8a98de60593d729c28bdcfe360eaad1" ], + "index": "pypi", "version": "==1.2.2" }, "alembic": { @@ -60,6 +61,14 @@ "index": "pypi", "version": "==0.60.0" }, + "azure-mgmt-consumption": { + "hashes": [ + "sha256:035d4b74ca7c47e2683bea17105fd9014c27060336fb6255324ac86b27f70f5b", + "sha256:af319ad6e3ec162a7578563f149e3cdd7d833a62ec80761cfd93caf79467610b" + ], + "index": "pypi", + "version": "==3.0.0" + }, "azure-mgmt-managementgroups": { "hashes": [ "sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682", diff --git a/atst/domain/csp/cloud.py b/atst/domain/csp/cloud.py index 9f764da0..b9323585 100644 --- a/atst/domain/csp/cloud.py +++ b/atst/domain/csp/cloud.py @@ -156,12 +156,31 @@ class TenantCSPPayload(BaseCSPPayload): country_code: str password_recovery_email_address: str + class Config: + fields = { + "user_id": "userId", + "domain_name": "domainName", + "first_name": "firstName", + "last_name": "lastName", + "country_code": "countryCode", + "password_recovery_email_address": "passwordRecoveryEmailAddress", + } + allow_population_by_field_name = True + class TenantCSPResult(BaseModel): user_id: str tenant_id: str user_object_id: str + class Config: + allow_population_by_field_name = True + fields = { + "user_id": "userId", + "tenant_id": "tenantId", + "user_object_id": "objectId", + } + class BillingProfileAddress(BaseModel): address: Dict @@ -558,11 +577,15 @@ class AzureSDKProvider(object): import azure.graphrbac as graphrbac import azure.common.credentials as credentials from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD + import adal + import requests self.subscription = subscription self.authorization = authorization + self.adal = adal self.graphrbac = graphrbac self.credentials = credentials + self.requests = requests # may change to a JEDI cloud self.cloud = AZURE_PUBLIC_CLOUD @@ -657,20 +680,31 @@ class AzureCloudProvider(CloudProviderInterface): "role_name": role_assignment_id, } - def create_tenant(self, payload): - # auth as SP that is allowed to create tenant? (tenant creation sp creds) - # create tenant with owner details (populated from portfolio point of contact, pw is generated) + def create_tenant(self, payload: TenantCSPPayload): + sp_token = self._get_sp_token(payload.creds) + if sp_token is None: + raise AuthenticationException("Could not resolve token for tenant creation") - # return tenant id, tenant owner id and tenant owner object id from: - response = {"tenantId": "string", "userId": "string", "objectId": "string"} - return self._ok( - { - "tenant_id": response["tenantId"], - "user_id": response["userId"], - "user_object_id": response["objectId"], - } + create_tenant_body = payload.dict(by_alias=True) + + print(create_tenant_body) + + create_tenant_headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {sp_token}", + } + + result = self.sdk.requests.post( + "https://management.azure.com/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview", + json=create_tenant_body, + headers=create_tenant_headers, ) + if result.status_code == 200: + return self._ok(TenantCSPResult(**result.json())) + else: + return self._error(result.json()) + def create_billing_owner(self, creds, tenant_admin_details): # authenticate as tenant_admin # create billing owner identity @@ -838,6 +872,26 @@ class AzureCloudProvider(CloudProviderInterface): if sub_id_match: return sub_id_match.group(1) + def _get_sp_token(self, creds): + home_tenant_id = creds.get("home_tenant_id") + client_id = creds.get("client_id") + secret_key = creds.get("secret_key") + + # TODO: Make endpoints consts or configs + authentication_endpoint = "https://login.microsoftonline.com/" + resource = "https://management.azure.com/" + + context = self.sdk.adal.AuthenticationContext( + authentication_endpoint + home_tenant_id + ) + + # TODO: handle failure states here + token_response = context.acquire_token_with_client_credentials( + resource, client_id, secret_key + ) + + return token_response.get("accessToken", None) + def _get_credential_obj(self, creds, resource=None): return self.sdk.credentials.ServicePrincipalCredentials( diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py index cc6beb5b..b6e0d380 100644 --- a/tests/domain/cloud/test_azure_csp.py +++ b/tests/domain/cloud/test_azure_csp.py @@ -3,7 +3,7 @@ from unittest.mock import Mock from uuid import uuid4 -from atst.domain.csp.cloud import AzureCloudProvider +from atst.domain.csp.cloud import AzureCloudProvider, TenantCSPResult from tests.mock_azure import mock_azure, AUTH_CREDENTIALS from tests.factories import EnvironmentFactory, ApplicationFactory @@ -121,3 +121,22 @@ def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider): policy_definition_name=properties.get("displayName"), parameters=mock_policy_definition, ) + + +def test_create_tenant(mock_azure: AzureCloudProvider): + mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = { + "accessToken": "TOKEN" + } + + mock_result = Mock() + mock_result.json.return_value = { + "objectId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d", + "tenantId": "60ff9d34-82bf-4f21-b565-308ef0533435", + "userId": "1153801116406515559", + } + mock_result.status_code = 200 + mock_azure.sdk.requests.post.return_value = mock_result + result = mock_azure.create_tenant(None, suffix=2) + print(result) + body: TenantCSPResult = result.get("body") + assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435" diff --git a/tests/mock_azure.py b/tests/mock_azure.py index 417e69fb..ecfafaac 100644 --- a/tests/mock_azure.py +++ b/tests/mock_azure.py @@ -53,16 +53,30 @@ def mock_policy(): return Mock(spec=policy) +def mock_adal(): + import adal + + return Mock(spec=adal) + + +def mock_requests(): + import requests + + return Mock(spec=requests) + + class MockAzureSDK(object): def __init__(self): from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD self.subscription = mock_subscription() self.authorization = mock_authorization() + self.adal = mock_adal() self.managementgroups = mock_managementgroups() self.graphrbac = mock_graphrbac() self.credentials = mock_credentials() self.policy = mock_policy() + self.requests = mock_requests() # may change to a JEDI cloud self.cloud = AZURE_PUBLIC_CLOUD