From 475ceaed7cb377651b61e27aa22b569cec2a38a1 Mon Sep 17 00:00:00 2001 From: tomdds Date: Mon, 27 Jan 2020 16:49:19 -0500 Subject: [PATCH 1/7] Source Azure Environment Values from Config This commit switches a few previously hardcoded values to be parsed from configuration, either from the SDK or current consts. --- atst/domain/csp/cloud/azure_cloud_provider.py | 33 +++++++++---------- tests/mock_azure.py | 10 ++++-- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/atst/domain/csp/cloud/azure_cloud_provider.py b/atst/domain/csp/cloud/azure_cloud_provider.py index 5a6a9253..4d3fb87e 100644 --- a/atst/domain/csp/cloud/azure_cloud_provider.py +++ b/atst/domain/csp/cloud/azure_cloud_provider.py @@ -27,16 +27,16 @@ from .models import ( ) from .policy import AzurePolicyManager -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, ) # This needs to be a fully pathed role definition identifier, not just a UUID +# TODO: Extract these from sdk msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD +AZURE_SKU_ID = "0001" # probably a static sku specific to ATAT/JEDI REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00000000-0000-4000-8000-000000000000" -AZURE_MANAGEMENT_API = "https://management.azure.com" class AzureSDKProvider(object): @@ -47,8 +47,6 @@ class AzureSDKProvider(object): import azure.common.credentials as credentials import azure.identity as identity from azure.keyvault import secrets - - from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD import adal import requests @@ -63,7 +61,10 @@ class AzureSDKProvider(object): self.exceptions = exceptions self.secrets = secrets self.requests = requests - # may change to a JEDI cloud + + # TODO: choose cloud type from config + from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD + self.cloud = AZURE_PUBLIC_CLOUD @@ -298,7 +299,7 @@ class AzureCloudProvider(CloudProviderInterface): } result = self.sdk.requests.post( - "https://management.azure.com/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview", + f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview", json=create_tenant_body, headers=create_tenant_headers, ) @@ -329,7 +330,7 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {sp_token}", } - billing_account_create_url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles?api-version=2019-10-01-preview" + billing_account_create_url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles?api-version=2019-10-01-preview" result = self.sdk.requests.post( billing_account_create_url, @@ -387,7 +388,7 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {sp_token}", } - url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/createBillingRoleAssignment?api-version=2019-10-01-preview" + url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/createBillingRoleAssignment?api-version=2019-10-01-preview" result = self.sdk.requests.post(url, headers=headers, json=request_body) if result.status_code == 201: @@ -403,7 +404,7 @@ class AzureCloudProvider(CloudProviderInterface): { "op": "replace", "path": "/enabledAzurePlans", - "value": [{"skuId": "0001"}], + "value": [{"skuId": AZURE_SKU_ID}], } ] @@ -411,7 +412,7 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {sp_token}", } - url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}?api-version=2019-10-01-preview" + url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}?api-version=2019-10-01-preview" result = self.sdk.requests.patch( url, headers=request_headers, json=request_body @@ -465,7 +466,7 @@ class AzureCloudProvider(CloudProviderInterface): } } - url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/instructions/{payload.initial_task_order_id}:CLIN00{payload.initial_clin_type}?api-version=2019-10-01-preview" + url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/instructions/{payload.initial_task_order_id}:CLIN00{payload.initial_clin_type}?api-version=2019-10-01-preview" auth_header = { "Authorization": f"Bearer {sp_token}", @@ -567,17 +568,13 @@ class AzureCloudProvider(CloudProviderInterface): 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 + f"{self.sdk.cloud.endpoints.active_directory}/{home_tenant_id}" ) # TODO: handle failure states here token_response = context.acquire_token_with_client_credentials( - resource, client_id, secret_key + self.sdk.cloud.endpoints.resource_manager, client_id, secret_key ) return token_response.get("accessToken", None) diff --git a/tests/mock_azure.py b/tests/mock_azure.py index 7fa67667..9ab1f61d 100644 --- a/tests/mock_azure.py +++ b/tests/mock_azure.py @@ -72,9 +72,14 @@ def mock_secrets(): return Mock(spec=secrets) +def mock_cloud_details(): + from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD + + return AZURE_PUBLIC_CLOUD + + class MockAzureSDK(object): def __init__(self): - from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD self.subscription = mock_subscription() self.authorization = mock_authorization() @@ -86,8 +91,7 @@ class MockAzureSDK(object): self.policy = mock_policy() self.secrets = mock_secrets() self.requests = mock_requests() - # may change to a JEDI cloud - self.cloud = AZURE_PUBLIC_CLOUD + self.cloud = mock_cloud_details() @pytest.fixture(scope="function") From 7bf6b9addc5a9eac44cea7a3a655f1bc6d9c3773 Mon Sep 17 00:00:00 2001 From: tomdds Date: Tue, 28 Jan 2020 14:12:04 -0500 Subject: [PATCH 2/7] Remove creds from payloads and passthroughs. --- atst/domain/csp/cloud/azure_cloud_provider.py | 29 ++++++++++++------- atst/domain/csp/cloud/models.py | 15 ++-------- tests/domain/cloud/test_azure_csp.py | 18 ++++-------- tests/domain/test_portfolio_state_machine.py | 4 +-- tests/mock_azure.py | 7 +++++ 5 files changed, 35 insertions(+), 38 deletions(-) diff --git a/atst/domain/csp/cloud/azure_cloud_provider.py b/atst/domain/csp/cloud/azure_cloud_provider.py index 4d3fb87e..703b5635 100644 --- a/atst/domain/csp/cloud/azure_cloud_provider.py +++ b/atst/domain/csp/cloud/azure_cloud_provider.py @@ -86,7 +86,7 @@ class AzureCloudProvider(CloudProviderInterface): def set_secret(self, secret_key, secret_value): credential = self._get_client_secret_credential_obj({}) - secret_client = self.secrets.SecretClient( + secret_client = self.sdk.secrets.SecretClient( vault_url=self.vault_url, credential=credential, ) try: @@ -99,7 +99,7 @@ class AzureCloudProvider(CloudProviderInterface): def get_secret(self, secret_key): credential = self._get_client_secret_credential_obj({}) - secret_client = self.secrets.SecretClient( + secret_client = self.sdk.secrets.SecretClient( vault_url=self.vault_url, credential=credential, ) try: @@ -288,7 +288,7 @@ class AzureCloudProvider(CloudProviderInterface): ) def create_tenant(self, payload: TenantCSPPayload): - sp_token = self._get_sp_token(payload.creds) + sp_token = self.get_root_provisioning_token() if sp_token is None: raise AuthenticationException("Could not resolve token for tenant creation") payload.password = token_urlsafe(16) @@ -318,7 +318,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_billing_profile_creation( self, payload: BillingProfileCreationCSPPayload ): - sp_token = self._get_sp_token(payload.creds) + sp_token = self.get_root_provisioning_token() if sp_token is None: raise AuthenticationException( "Could not resolve token for billing profile creation" @@ -350,7 +350,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_billing_profile_verification( self, payload: BillingProfileVerificationCSPPayload ): - sp_token = self._get_sp_token(payload.creds) + sp_token = self.get_root_provisioning_token() if sp_token is None: raise AuthenticationException( "Could not resolve token for billing profile validation" @@ -375,7 +375,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_billing_profile_tenant_access( self, payload: BillingProfileTenantAccessCSPPayload ): - sp_token = self._get_sp_token(payload.creds) + sp_token = self.get_root_provisioning_token() request_body = { "properties": { "principalTenantId": payload.tenant_id, # from tenant creation @@ -399,7 +399,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_task_order_billing_creation( self, payload: TaskOrderBillingCreationCSPPayload ): - sp_token = self._get_sp_token(payload.creds) + sp_token = self.get_root_provisioning_token() request_body = [ { "op": "replace", @@ -429,7 +429,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_task_order_billing_verification( self, payload: TaskOrderBillingVerificationCSPPayload ): - sp_token = self._get_sp_token(payload.creds) + sp_token = self.get_root_provisioning_token() if sp_token is None: raise AuthenticationException( "Could not resolve token for task order billing validation" @@ -452,7 +452,7 @@ class AzureCloudProvider(CloudProviderInterface): return self._error(result.json()) def create_billing_instruction(self, payload: BillingInstructionCSPPayload): - sp_token = self._get_sp_token(payload.creds) + sp_token = self.get_root_provisioning_token() if sp_token is None: raise AuthenticationException( "Could not resolve token for task order billing validation" @@ -563,13 +563,20 @@ class AzureCloudProvider(CloudProviderInterface): if sub_id_match: return sub_id_match.group(1) + def get_tenant_principal_token(self, tenant_id): + creds = self.get_secret(tenant_id) + return self._get_sp_token(creds) + + def get_root_provisioning_token(self): + return self._get_sp_token(self._root_creds) + def _get_sp_token(self, creds): - home_tenant_id = creds.get("home_tenant_id") + tenant_id = creds.get("tenant_id") client_id = creds.get("client_id") secret_key = creds.get("secret_key") context = self.sdk.adal.AuthenticationContext( - f"{self.sdk.cloud.endpoints.active_directory}/{home_tenant_id}" + f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}" ) # TODO: handle failure states here diff --git a/atst/domain/csp/cloud/models.py b/atst/domain/csp/cloud/models.py index c6bf0ede..f6503445 100644 --- a/atst/domain/csp/cloud/models.py +++ b/atst/domain/csp/cloud/models.py @@ -20,20 +20,10 @@ class AliasModel(BaseModel): class BaseCSPPayload(AliasModel): - # {"username": "mock-cloud", "pass": "shh"} - creds: Dict - - def dict(self, *args, **kwargs): - exclude = {"creds"} - if "exclude" not in kwargs: - kwargs["exclude"] = exclude - else: - kwargs["exclude"].update(exclude) - - return super().dict(*args, **kwargs) + tenant_id: str -class TenantCSPPayload(BaseCSPPayload): +class TenantCSPPayload(AliasModel): user_id: str password: Optional[str] domain_name: str @@ -232,3 +222,4 @@ class BillingInstructionCSPResult(AliasModel): fields = { "reported_clin_name": "name", } + diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py index 0648ec1e..28a8dcf1 100644 --- a/tests/domain/cloud/test_azure_csp.py +++ b/tests/domain/cloud/test_azure_csp.py @@ -22,11 +22,6 @@ from atst.domain.csp.cloud.models import ( TenantCSPResult, ) -creds = { - "home_tenant_id": "tenant_id", - "client_id": "client_id", - "secret_key": "secret_key", -} BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31" @@ -146,7 +141,7 @@ def test_create_tenant(mock_azure: AzureCloudProvider): mock_azure.sdk.requests.post.return_value = mock_result payload = TenantCSPPayload( **dict( - creds=creds, + tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", user_id="admin", password="JediJan13$coot", # pragma: allowlist secret domain_name="jediccpospawnedtenant2", @@ -183,7 +178,6 @@ def test_create_billing_profile_creation(mock_azure: AzureCloudProvider): country="US", postal_code="19109", ), - creds=creds, tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", billing_profile_display_name="Test Billing Profile", billing_account_name=BILLING_ACCOUNT_NAME, @@ -234,7 +228,7 @@ def test_validate_billing_profile_creation(mock_azure: AzureCloudProvider): payload = BillingProfileVerificationCSPPayload( **dict( - creds=creds, + tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", billing_profile_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview", ) ) @@ -273,7 +267,6 @@ def test_create_billing_profile_tenant_access(mock_azure: AzureCloudProvider): payload = BillingProfileTenantAccessCSPPayload( **dict( - creds=creds, tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", user_object_id="0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d", billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31", @@ -305,7 +298,7 @@ def test_create_task_order_billing_creation(mock_azure: AzureCloudProvider): payload = TaskOrderBillingCreationCSPPayload( **dict( - creds=creds, + tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31", billing_profile_name="KQWI-W2SU-BG7-TGB", ) @@ -365,7 +358,7 @@ def test_create_task_order_billing_verification(mock_azure): payload = TaskOrderBillingVerificationCSPPayload( **dict( - creds=creds, + tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", task_order_billing_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview", ) ) @@ -400,7 +393,7 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider): payload = BillingInstructionCSPPayload( **dict( - creds=creds, + tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", initial_clin_amount=1000.00, initial_clin_start_date="2020/1/1", initial_clin_end_date="2020/3/1", @@ -413,3 +406,4 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider): result = mock_azure.create_billing_instruction(payload) body: BillingInstructionCSPResult = result.get("body") assert body.reported_clin_name == "TO1:CLIN001" + diff --git a/tests/domain/test_portfolio_state_machine.py b/tests/domain/test_portfolio_state_machine.py index 2e412653..44d1382b 100644 --- a/tests/domain/test_portfolio_state_machine.py +++ b/tests/domain/test_portfolio_state_machine.py @@ -106,8 +106,6 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio): FSMStates.BILLING_INSTRUCTION_CREATED, ] - # Should source all creds for portfolio? might be easier to manage than per-step specific ones - creds = {"username": "mock-cloud", "password": "shh"} # pragma: allowlist secret if portfolio.csp_data is not None: csp_data = portfolio.csp_data else: @@ -150,7 +148,7 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio): collected_data = dict( list(csp_data.items()) + list(portfolio_data.items()) + list(config.items()) ) - sm.trigger_next_transition(creds=creds, csp_data=collected_data) + sm.trigger_next_transition(csp_data=collected_data) assert sm.state == expected_state if portfolio.csp_data is not None: csp_data = portfolio.csp_data diff --git a/tests/mock_azure.py b/tests/mock_azure.py index 9ab1f61d..4a7aace3 100644 --- a/tests/mock_azure.py +++ b/tests/mock_azure.py @@ -48,6 +48,12 @@ def mock_credentials(): return Mock(spec=credentials) +def mock_identity(): + import azure.identity as identity + + return Mock(spec=identity) + + def mock_policy(): from azure.mgmt.resource import policy @@ -88,6 +94,7 @@ class MockAzureSDK(object): self.managementgroups = mock_managementgroups() self.graphrbac = mock_graphrbac() self.credentials = mock_credentials() + self.identity = mock_identity() self.policy = mock_policy() self.secrets = mock_secrets() self.requests = mock_requests() From 144312863cc984ffbd06a4297244d3b793fa08c9 Mon Sep 17 00:00:00 2001 From: tomdds Date: Tue, 28 Jan 2020 20:14:42 -0500 Subject: [PATCH 3/7] Draft implementations of remote admin creation and root management group ownership. --- atst/domain/csp/cloud/azure_cloud_provider.py | 227 ++++++++++++++++-- tests/domain/cloud/test_azure_csp.py | 47 +++- tests/mock_azure.py | 6 +- 3 files changed, 261 insertions(+), 19 deletions(-) diff --git a/atst/domain/csp/cloud/azure_cloud_provider.py b/atst/domain/csp/cloud/azure_cloud_provider.py index 703b5635..8af2580a 100644 --- a/atst/domain/csp/cloud/azure_cloud_provider.py +++ b/atst/domain/csp/cloud/azure_cloud_provider.py @@ -1,4 +1,5 @@ import re +import time from secrets import token_urlsafe from typing import Dict from uuid import uuid4 @@ -76,6 +77,8 @@ class AzureCloudProvider(CloudProviderInterface): self.secret_key = config["AZURE_SECRET_KEY"] self.tenant_id = config["AZURE_TENANT_ID"] self.vault_url = config["AZURE_VAULT_URL"] + # self.ps_client_id = config["POWERSHELL_CLIENT_ID"] + self.ps_client_id = "1950a258-227b-4e31-a9cf-717495945fc2" if azure_sdk_provider is None: self.sdk = AzureSDKProvider() @@ -479,21 +482,188 @@ class AzureCloudProvider(CloudProviderInterface): else: return self._error(result.json()) - def create_remote_admin(self, creds, tenant_details): - # create app/service principal within tenant, with name constructed from tenant details - # assign principal global admin - - # needs to call out to CLI with tenant owner username/password, prototyping for that underway - - # return identifier and creds to consumer for storage - response = {"clientId": "string", "secretKey": "string", "tenantId": "string"} - return self._ok( - { - "client_id": response["clientId"], - "secret_key": response["secret_key"], - "tenant_id": response["tenantId"], - } + def assign_root_mg_ownership(self, payload): + import ipdb; ipdb.set_trace() + # elevate + mgmt_token = self.get_tenant_admin_token( + payload.tenant_id, self.sdk.cloud.endpoints.resource_manager ) + if mgmt_token is None: + raise AuthenticationException( + "Could not resolve management token for tenant admin" + ) + + auth_header = { + "Authorization": f"Bearer {mgmt_token}", + } + url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01" + result = self.sdk.requests.post(url, headers=auth_header) + + if not result.ok: + return False + + # ----------- NEXT STEP: Root MGMT Group Ownership (tenant admin) ------------- + time.sleep(20) + # HARD CODED, MOVE TO CONFIG + ownerRoleId = '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' + + role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{ownerRoleId}" + + request_body = { + "properties": { + "roleDefinitionId": role_definition_id, + "principalId": payload.user_object_id + } + } + + auth_header = { + "Authorization": f"Bearer {mgmt_token}", + } + + assignment_guid = str(uuid4()) + + url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01" + + response = self.sdk.requests.put(url, headers=auth_header, json=request_body) + + if not response.ok: + return False + + # ----------- NEXT STEP: Root MGMT Group Ownership (remote admin SP) ------------- + time.sleep(20) + # HARD CODED, MOVE TO CONFIG + ownerRoleId = '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' + + # NOTE: the tenant_id is also the id of the root management group, once it is created + role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{ownerRoleId}" + + request_body = { + "properties": { + "roleDefinitionId": role_definition_id, + "principalId": payload.admin_principal_id + } + } + + auth_header = { + "Authorization": f"Bearer {mgmt_token}", + } + + assignment_guid = str(uuid4()) + + url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01" + + response = self.sdk.requests.put(url, headers=auth_header, json=request_body) + + if not response.ok: + return False + + + + def create_remote_admin(self, payload): + import ipdb; ipdb.set_trace() + GRAPH_RESOURCE = "https://graph.microsoft.com" + graph_token = self.get_tenant_admin_token(payload.tenant_id, GRAPH_RESOURCE) + if graph_token is None: + raise AuthenticationException( + "Could not resolve graph token for tenant admin" + ) + + request_body = {"displayName": "ATAT Remote Admin"} + + auth_header = { + "Authorization": f"Bearer {graph_token}", + } + + url = f"{GRAPH_RESOURCE}/v1.0/applications" + + response = self.sdk.requests.post(url, json=request_body, headers=auth_header) + + res = response.json() + result1 = {} + if response.ok: + result1 = {"app_id": res.get("appId"), "object_id": res.get("id")} + + # ---- SEPARATE STEP (Create associated Service Principal) ---------- + time.sleep(20) + + request_body = {"appId": result1.get("app_id")} + + auth_header = { + "Authorization": f"Bearer {graph_token}", + } + + url = f"{GRAPH_RESOURCE}/beta/servicePrincipals" + + response = self.sdk.requests.post(url, json=request_body, headers=auth_header) + + res = response.json() + result2 = {} + if response.ok: + result2 = {"sp_id": res.get("id")} + + # ---- SEPARATE STEP - Generate Creds (Client Secret)---------- + time.sleep(20) + + request_body = { + "passwordCredentials": [{"displayName": "ATAT Generated Password"}] + } + + auth_header = { + "Authorization": f"Bearer {graph_token}", + } + + # Uses OBJECT_ID of App Registration + url = ( + f"{GRAPH_RESOURCE}/v1.0/applications/{result1.get('object_id')}/addPassword" + ) + + response = self.sdk.requests.post(url, json=request_body, headers=auth_header) + result3 = {} + res = response.json() + if response.ok: + result3 = {"client_secret": res.get("secretText")} + + # ---- SEPARATE STEP - Source Global Admin Role---------- + + auth_header = { + "Authorization": f"Bearer {graph_token}", + } + + # Uses OBJECT_ID of App Registration + url = f"{GRAPH_RESOURCE}/beta/roleManagement/directory/roleDefinitions" + + response = self.sdk.requests.get(url, headers=auth_header) + + result = response.json() + roleList = result.get("value") + + admin_role_id = "794bb258-3e31-42ff-9ee4-731a72f62851" # May be hard coded? use for fall back + for role in roleList: + if role.get("displayName") == "Company Administrator": + admin_role_id = role.get("id") + break + + # ---- SEPARATE STEP - Source Global Admin Role---------- + time.sleep(20) + + request_body = { + "principalId": result2.get("sp_id"), + "roleDefinitionId": admin_role_id, + "resourceScope": "/", + } + + auth_header = { + "Authorization": f"Bearer {graph_token}", + } + + url = f"{GRAPH_RESOURCE}/beta/roleManagement/directory/roleAssignments" + + response = self.sdk.requests.post(url, headers=auth_header, json=request_body) + + if response.ok: + return (result1, result2, result3) + + return False def force_tenant_admin_pw_update(self, creds, tenant_owner_id): # use creds to update to force password recovery? @@ -563,9 +733,21 @@ class AzureCloudProvider(CloudProviderInterface): if sub_id_match: return sub_id_match.group(1) - def get_tenant_principal_token(self, tenant_id): + def get_tenant_admin_token(self, tenant_id, resource): creds = self.get_secret(tenant_id) - return self._get_sp_token(creds) + return self._get_up_token_for_resource( + creds.get("admin_username"), creds.get("admin_password"), tenant_id, resource + ) + + def get_tenant_principal_token(self, tenant_id, resource): + # creds = self.get_secret(tenant_id) + # return self._get_up_token_for_resource( + # creds.get("admin_username"), + # creds.get("admin_password"), + # tenat_id, + # resource + # ) + pass def get_root_provisioning_token(self): return self._get_sp_token(self._root_creds) @@ -586,6 +768,19 @@ class AzureCloudProvider(CloudProviderInterface): return token_response.get("accessToken", None) + def _get_up_token_for_resource(self, username, password, tenant_id, resource): + + context = self.sdk.adal.AuthenticationContext( + f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}" + ) + + # TODO: handle failure states here + token_response = context.acquire_token_with_username_password( + resource, username, password, self.ps_client_id + ) + + return token_response.get("accessToken", None) + def _get_credential_obj(self, creds, resource=None): return self.sdk.credentials.ServicePrincipalCredentials( client_id=creds.get("client_id"), diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py index 28a8dcf1..20476135 100644 --- a/tests/domain/cloud/test_azure_csp.py +++ b/tests/domain/cloud/test_azure_csp.py @@ -1,4 +1,4 @@ -from unittest.mock import Mock +from unittest.mock import Mock, patch from uuid import uuid4 from tests.factories import ApplicationFactory, EnvironmentFactory @@ -6,6 +6,7 @@ from tests.mock_azure import AUTH_CREDENTIALS, mock_azure from atst.domain.csp.cloud import AzureCloudProvider from atst.domain.csp.cloud.models import ( + BaseCSPPayload, BillingInstructionCSPPayload, BillingInstructionCSPResult, BillingProfileCreationCSPPayload, @@ -407,3 +408,47 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider): body: BillingInstructionCSPResult = result.get("body") assert body.reported_clin_name == "TO1:CLIN001" + +def test_admin_principal_creation(mock_azure: AzureCloudProvider): + # Auth As Tenant Admin + # Create App Registration + # Create Service Principal + # Create App Registration Password Credential + # Lookup global admin role + # Assign global admin role to Service Principal + with patch.object( + AzureCloudProvider, "get_secret", wraps=mock_azure.get_secret + ) as mock_get_secret: + mock_get_secret.return_value = { + "admin_username": "", + "admin_password": "", + } + payload = BaseCSPPayload( + **{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"} + ) + + result = mock_azure.create_remote_admin(payload) + + print(result) + + +def test_admin_mg_ownership(mock_azure: AzureCloudProvider): + with patch.object( + AzureCloudProvider, "get_secret", wraps=mock_azure.get_secret + ) as mock_get_secret: + mock_get_secret.return_value = { + "admin_username": "", + "admin_password": "", + } + payload = TenantCSPResult( + **{ + "user_id": "blach", + "tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4", + "user_object_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d", + } + ) + + result = mock_azure.assign_root_mg_ownership(payload) + + print(result) + diff --git a/tests/mock_azure.py b/tests/mock_azure.py index 4a7aace3..9963f2d9 100644 --- a/tests/mock_azure.py +++ b/tests/mock_azure.py @@ -63,13 +63,15 @@ def mock_policy(): def mock_adal(): import adal - return Mock(spec=adal) + return adal + # return Mock(spec=adal) def mock_requests(): import requests - return Mock(spec=requests) + # return Mock(spec=requests) + return requests def mock_secrets(): From d4dd581b7ae3f449f89ddfe785c07ea0758d119b Mon Sep 17 00:00:00 2001 From: tomdds Date: Wed, 29 Jan 2020 16:17:28 -0500 Subject: [PATCH 4/7] Implement principal creation and admin elevation provisioning features. --- atst/domain/csp/cloud/azure_cloud_provider.py | 175 +++++++++------- atst/domain/csp/cloud/mock_cloud_provider.py | 94 ++++++++- atst/domain/csp/cloud/models.py | 77 ++++++++ atst/models/mixins/state_machines.py | 7 + tests/domain/cloud/test_azure_csp.py | 187 +++++++++++++++--- tests/domain/test_portfolio_state_machine.py | 7 + tests/mock_azure.py | 9 +- 7 files changed, 447 insertions(+), 109 deletions(-) diff --git a/atst/domain/csp/cloud/azure_cloud_provider.py b/atst/domain/csp/cloud/azure_cloud_provider.py index 8af2580a..73a99738 100644 --- a/atst/domain/csp/cloud/azure_cloud_provider.py +++ b/atst/domain/csp/cloud/azure_cloud_provider.py @@ -1,5 +1,4 @@ import re -import time from secrets import token_urlsafe from typing import Dict from uuid import uuid4 @@ -11,6 +10,8 @@ from atst.models.user import User from .cloud_provider_interface import CloudProviderInterface from .exceptions import AuthenticationException from .models import ( + AdminRoleDefinitionCSPPayload, + AdminRoleDefinitionCSPResult, BillingInstructionCSPPayload, BillingInstructionCSPResult, BillingProfileCreationCSPPayload, @@ -19,16 +20,27 @@ from .models import ( BillingProfileTenantAccessCSPResult, BillingProfileVerificationCSPPayload, BillingProfileVerificationCSPResult, + PrincipalAdminRoleCSPPayload, + PrincipalAdminRoleCSPResult, TaskOrderBillingCreationCSPPayload, TaskOrderBillingCreationCSPResult, TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPResult, + TenantAdminOwnershipCSPPayload, + TenantAdminOwnershipCSPResult, TenantCSPPayload, TenantCSPResult, + TenantPrincipalAppCSPPayload, + TenantPrincipalAppCSPResult, + TenantPrincipalCredentialCSPPayload, + TenantPrincipalCredentialCSPResult, + TenantPrincipalCSPPayload, + TenantPrincipalCSPResult, + TenantPrincipalOwnershipCSPPayload, + TenantPrincipalOwnershipCSPResult, ) from .policy import AzurePolicyManager - 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, @@ -77,8 +89,9 @@ class AzureCloudProvider(CloudProviderInterface): self.secret_key = config["AZURE_SECRET_KEY"] self.tenant_id = config["AZURE_TENANT_ID"] self.vault_url = config["AZURE_VAULT_URL"] - # self.ps_client_id = config["POWERSHELL_CLIENT_ID"] - self.ps_client_id = "1950a258-227b-4e31-a9cf-717495945fc2" + self.ps_client_id = config["POWERSHELL_CLIENT_ID"] + self.owner_role_def_id = config["AZURE_OWNER_ROLE_DEF_ID"] + self.graph_resource = config["AZURE_GRAPH_RESOURCE"] if azure_sdk_provider is None: self.sdk = AzureSDKProvider() @@ -482,15 +495,13 @@ class AzureCloudProvider(CloudProviderInterface): else: return self._error(result.json()) - def assign_root_mg_ownership(self, payload): - import ipdb; ipdb.set_trace() - # elevate + def get_elevated_management_token(self, tenant_id): mgmt_token = self.get_tenant_admin_token( - payload.tenant_id, self.sdk.cloud.endpoints.resource_manager + tenant_id, self.sdk.cloud.endpoints.resource_manager ) if mgmt_token is None: raise AuthenticationException( - "Could not resolve management token for tenant admin" + "Failed to resolve management token for tenant admin" ) auth_header = { @@ -500,19 +511,19 @@ class AzureCloudProvider(CloudProviderInterface): result = self.sdk.requests.post(url, headers=auth_header) if not result.ok: - return False + raise AuthenticationException("Failed to elevate access") - # ----------- NEXT STEP: Root MGMT Group Ownership (tenant admin) ------------- - time.sleep(20) - # HARD CODED, MOVE TO CONFIG - ownerRoleId = '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' + return mgmt_token - role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{ownerRoleId}" + def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload): + mgmt_token = self.get_elevated_management_token(payload.tenant_id) + + role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}" request_body = { "properties": { "roleDefinitionId": role_definition_id, - "principalId": payload.user_object_id + "principalId": payload.user_object_id, } } @@ -526,21 +537,21 @@ class AzureCloudProvider(CloudProviderInterface): response = self.sdk.requests.put(url, headers=auth_header, json=request_body) - if not response.ok: - return False + if response.ok: + return TenantAdminOwnershipCSPResult(**response.json()) - # ----------- NEXT STEP: Root MGMT Group Ownership (remote admin SP) ------------- - time.sleep(20) - # HARD CODED, MOVE TO CONFIG - ownerRoleId = '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' + def create_tenant_principal_ownership( + self, payload: TenantPrincipalOwnershipCSPPayload + ): + mgmt_token = self.get_elevated_management_token(payload.tenant_id) # NOTE: the tenant_id is also the id of the root management group, once it is created - role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{ownerRoleId}" + role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}" request_body = { "properties": { "roleDefinitionId": role_definition_id, - "principalId": payload.admin_principal_id + "principalId": payload.principal_id, } } @@ -554,15 +565,14 @@ class AzureCloudProvider(CloudProviderInterface): response = self.sdk.requests.put(url, headers=auth_header, json=request_body) - if not response.ok: - return False + if response.ok: + return TenantPrincipalOwnershipCSPResult(**response.json()) + def create_tenant_principal_app(self, payload: TenantPrincipalAppCSPPayload): - - def create_remote_admin(self, payload): - import ipdb; ipdb.set_trace() - GRAPH_RESOURCE = "https://graph.microsoft.com" - graph_token = self.get_tenant_admin_token(payload.tenant_id, GRAPH_RESOURCE) + graph_token = self.get_tenant_admin_token( + payload.tenant_id, self.graph_resource + ) if graph_token is None: raise AuthenticationException( "Could not resolve graph token for tenant admin" @@ -574,35 +584,45 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {graph_token}", } - url = f"{GRAPH_RESOURCE}/v1.0/applications" + url = f"{self.graph_resource}/v1.0/applications" response = self.sdk.requests.post(url, json=request_body, headers=auth_header) - res = response.json() - result1 = {} if response.ok: - result1 = {"app_id": res.get("appId"), "object_id": res.get("id")} + return TenantPrincipalAppCSPResult(**response.json()) - # ---- SEPARATE STEP (Create associated Service Principal) ---------- - time.sleep(20) + def create_tenant_principal(self, payload: TenantPrincipalCSPPayload): + graph_token = self.get_tenant_admin_token( + payload.tenant_id, self.graph_resource + ) + if graph_token is None: + raise AuthenticationException( + "Could not resolve graph token for tenant admin" + ) - request_body = {"appId": result1.get("app_id")} + request_body = {"appId": payload.principal_app_id} auth_header = { "Authorization": f"Bearer {graph_token}", } - url = f"{GRAPH_RESOURCE}/beta/servicePrincipals" + url = f"{self.graph_resource}/beta/servicePrincipals" response = self.sdk.requests.post(url, json=request_body, headers=auth_header) - res = response.json() - result2 = {} if response.ok: - result2 = {"sp_id": res.get("id")} + return TenantPrincipalCSPResult(**response.json()) - # ---- SEPARATE STEP - Generate Creds (Client Secret)---------- - time.sleep(20) + def create_tenant_principal_credential( + self, payload: TenantPrincipalCredentialCSPPayload + ): + graph_token = self.get_tenant_admin_token( + payload.tenant_id, self.graph_resource + ) + if graph_token is None: + raise AuthenticationException( + "Could not resolve graph token for tenant admin" + ) request_body = { "passwordCredentials": [{"displayName": "ATAT Generated Password"}] @@ -612,43 +632,59 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {graph_token}", } - # Uses OBJECT_ID of App Registration - url = ( - f"{GRAPH_RESOURCE}/v1.0/applications/{result1.get('object_id')}/addPassword" - ) + url = f"{self.graph_resource}/v1.0/applications/{payload.principal_app_object_id}/addPassword" response = self.sdk.requests.post(url, json=request_body, headers=auth_header) - result3 = {} - res = response.json() - if response.ok: - result3 = {"client_secret": res.get("secretText")} - # ---- SEPARATE STEP - Source Global Admin Role---------- + if response.ok: + return TenantPrincipalCredentialCSPResult( + principal_client_id=payload.principal_app_id, **response.json() + ) + + def create_admin_role_definition(self, payload: AdminRoleDefinitionCSPPayload): + graph_token = self.get_tenant_admin_token( + payload.tenant_id, self.graph_resource + ) + if graph_token is None: + raise AuthenticationException( + "Could not resolve graph token for tenant admin" + ) auth_header = { "Authorization": f"Bearer {graph_token}", } - # Uses OBJECT_ID of App Registration - url = f"{GRAPH_RESOURCE}/beta/roleManagement/directory/roleDefinitions" + url = f"{self.graph_resource}/beta/roleManagement/directory/roleDefinitions" response = self.sdk.requests.get(url, headers=auth_header) result = response.json() roleList = result.get("value") - admin_role_id = "794bb258-3e31-42ff-9ee4-731a72f62851" # May be hard coded? use for fall back - for role in roleList: - if role.get("displayName") == "Company Administrator": - admin_role_id = role.get("id") - break + DEFAULT_ADMIN_RD_ID = "794bb258-3e31-42ff-9ee4-731a72f62851" + admin_role_def_id = next( + ( + role.get("id") + for role in roleList + if role.get("displayName") == "Company Administrator" + ), + DEFAULT_ADMIN_RD_ID, + ) - # ---- SEPARATE STEP - Source Global Admin Role---------- - time.sleep(20) + return AdminRoleDefinitionCSPResult(admin_role_def_id=admin_role_def_id) + + def create_principal_admin_role(self, payload: PrincipalAdminRoleCSPPayload): + graph_token = self.get_tenant_admin_token( + payload.tenant_id, self.graph_resource + ) + if graph_token is None: + raise AuthenticationException( + "Could not resolve graph token for tenant admin" + ) request_body = { - "principalId": result2.get("sp_id"), - "roleDefinitionId": admin_role_id, + "principalId": payload.principal_id, + "roleDefinitionId": payload.admin_role_def_id, "resourceScope": "/", } @@ -656,14 +692,12 @@ class AzureCloudProvider(CloudProviderInterface): "Authorization": f"Bearer {graph_token}", } - url = f"{GRAPH_RESOURCE}/beta/roleManagement/directory/roleAssignments" + url = f"{self.graph_resource}/beta/roleManagement/directory/roleAssignments" response = self.sdk.requests.post(url, headers=auth_header, json=request_body) if response.ok: - return (result1, result2, result3) - - return False + return PrincipalAdminRoleCSPResult(**response.json()) def force_tenant_admin_pw_update(self, creds, tenant_owner_id): # use creds to update to force password recovery? @@ -736,7 +770,10 @@ class AzureCloudProvider(CloudProviderInterface): def get_tenant_admin_token(self, tenant_id, resource): creds = self.get_secret(tenant_id) return self._get_up_token_for_resource( - creds.get("admin_username"), creds.get("admin_password"), tenant_id, resource + creds.get("admin_username"), + creds.get("admin_password"), + tenant_id, + resource, ) def get_tenant_principal_token(self, tenant_id, resource): diff --git a/atst/domain/csp/cloud/mock_cloud_provider.py b/atst/domain/csp/cloud/mock_cloud_provider.py index a6c338b5..81147885 100644 --- a/atst/domain/csp/cloud/mock_cloud_provider.py +++ b/atst/domain/csp/cloud/mock_cloud_provider.py @@ -1,34 +1,46 @@ from uuid import uuid4 -from atst.domain.csp.cloud.exceptions import ( - BaselineProvisionException, - EnvironmentCreationException, - GeneralCSPException, - UserProvisioningException, - UserRemovalException, -) -from atst.domain.csp.cloud.models import BillingProfileTenantAccessCSPResult from .cloud_provider_interface import CloudProviderInterface from .exceptions import ( AuthenticationException, AuthorizationException, + BaselineProvisionException, ConnectionException, + EnvironmentCreationException, + GeneralCSPException, UnknownServerException, + UserProvisioningException, + UserRemovalException, ) from .models import ( + PrincipalAdminRoleCSPResult, + AdminRoleDefinitionCSPResult, + TenantAdminOwnershipCSPResult, + TenantPrincipalCSPResult, BillingInstructionCSPPayload, BillingInstructionCSPResult, BillingProfileCreationCSPPayload, BillingProfileCreationCSPResult, + BillingProfileTenantAccessCSPResult, BillingProfileVerificationCSPPayload, BillingProfileVerificationCSPResult, + PrincipalAdminRoleCSPPayload, + AdminRoleDefinitionCSPPayload, TaskOrderBillingCreationCSPPayload, TaskOrderBillingCreationCSPResult, TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPResult, + TenantAdminOwnershipCSPPayload, TenantCSPPayload, TenantCSPResult, + TenantPrincipalAppCSPPayload, + TenantPrincipalAppCSPResult, + TenantPrincipalCredentialCSPPayload, + TenantPrincipalCredentialCSPResult, + TenantPrincipalCSPPayload, + TenantPrincipalOwnershipCSPPayload, + TenantPrincipalOwnershipCSPResult, ) @@ -117,7 +129,7 @@ class MockCloudProvider(CloudProviderInterface): payload is an instance of TenantCSPPayload data class """ - self._authorize(payload.creds) + self._authorize("admin") self._delay(1, 5) @@ -274,6 +286,70 @@ class MockCloudProvider(CloudProviderInterface): } ) + def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload): + self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) + + return TenantAdminOwnershipCSPResult(**dict(id="admin_owner_assignment_id")) + + def create_tenant_principal_ownership( + self, payload: TenantPrincipalOwnershipCSPPayload + ): + self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) + + return TenantPrincipalOwnershipCSPResult( + **dict(id="principal_owner_assignment_id") + ) + + def create_tenant_principal_app(self, payload: TenantPrincipalAppCSPPayload): + self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) + + return TenantPrincipalAppCSPResult( + **dict(appId="principal_app_id", id="principal_app_object_id") + ) + + def create_tenant_principal(self, payload: TenantPrincipalCSPPayload): + self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) + + return TenantPrincipalCSPResult(**dict(id="principal_id")) + + def create_tenant_principal_credential( + self, payload: TenantPrincipalCredentialCSPPayload + ): + self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) + + return TenantPrincipalCredentialCSPResult( + **dict( + secretText="principal_secret_key", + principal_client_id="principal_client_id", + ) + ) + + def create_admin_role_definition(self, payload: AdminRoleDefinitionCSPPayload): + self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) + + return AdminRoleDefinitionCSPResult( + **dict(admin_role_def_id="admin_role_def_id") + ) + + def create_principal_admin_role(self, payload: PrincipalAdminRoleCSPPayload): + self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION) + self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION) + self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION) + + return PrincipalAdminRoleCSPResult(**dict(id="principal_assignment_id")) + def create_or_update_user(self, auth_credentials, user_info, csp_role_id): self._authorize(auth_credentials) diff --git a/atst/domain/csp/cloud/models.py b/atst/domain/csp/cloud/models.py index f6503445..9aa16883 100644 --- a/atst/domain/csp/cloud/models.py +++ b/atst/domain/csp/cloud/models.py @@ -223,3 +223,80 @@ class BillingInstructionCSPResult(AliasModel): "reported_clin_name": "name", } + +class TenantAdminOwnershipCSPPayload(BaseCSPPayload): + user_object_id: str + + +class TenantAdminOwnershipCSPResult(AliasModel): + admin_owner_assignment_id: str + + class Config: + fields = {"admin_owner_assignment_id": "id"} + + +class TenantPrincipalOwnershipCSPPayload(BaseCSPPayload): + principal_id: str + + +class TenantPrincipalOwnershipCSPResult(AliasModel): + principal_owner_assignment_id: str + + class Config: + fields = {"principal_owner_assignment_id": "id"} + + +class TenantPrincipalAppCSPPayload(BaseCSPPayload): + pass + + +class TenantPrincipalAppCSPResult(AliasModel): + principal_app_id: str + principal_app_object_id: str + + class Config: + fields = {"principal_app_id": "appId", "principal_app_object_id": "id"} + + +class TenantPrincipalCSPPayload(BaseCSPPayload): + principal_app_id: str + + +class TenantPrincipalCSPResult(AliasModel): + principal_id: str + + class Config: + fields = {"principal_id": "id"} + + +class TenantPrincipalCredentialCSPPayload(BaseCSPPayload): + principal_app_id: str + principal_app_object_id: str + + +class TenantPrincipalCredentialCSPResult(AliasModel): + principal_client_id: str + principal_secret_key: str + + class Config: + fields = {"principal_secret_key": "secretText"} + + +class AdminRoleDefinitionCSPPayload(BaseCSPPayload): + pass + + +class AdminRoleDefinitionCSPResult(AliasModel): + admin_role_def_id: str + + +class PrincipalAdminRoleCSPPayload(BaseCSPPayload): + principal_id: str + admin_role_def_id: str + + +class PrincipalAdminRoleCSPResult(AliasModel): + principal_assignment_id: str + + class Config: + fields = {"principal_assignment_id": "id"} diff --git a/atst/models/mixins/state_machines.py b/atst/models/mixins/state_machines.py index 37682e5b..889b5b8f 100644 --- a/atst/models/mixins/state_machines.py +++ b/atst/models/mixins/state_machines.py @@ -17,6 +17,13 @@ class AzureStages(Enum): TASK_ORDER_BILLING_CREATION = "task order billing creation" TASK_ORDER_BILLING_VERIFICATION = "task order billing verification" BILLING_INSTRUCTION = "billing instruction" + TENANT_PRINCIPAL_APP = "tenant principal application" + TENANT_PRINCIPAL = "tenant principal" + TENANT_PRINCIPAL_CREDENTIAL = "tenant principal credential" + ADMIN_ROLE_DEFINITION = "admin role definition" + PRINCIPAL_ADMIN_ROLE = "tenant principal admin" + TENANT_ADMIN_OWNERSHIP = "tenant admin ownership" + TENANT_PRINCIPAL_OWNERSHIP = "tenant principial ownership" def _build_csp_states(csp_stages): diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py index 20476135..da40634d 100644 --- a/tests/domain/cloud/test_azure_csp.py +++ b/tests/domain/cloud/test_azure_csp.py @@ -6,6 +6,8 @@ from tests.mock_azure import AUTH_CREDENTIALS, mock_azure from atst.domain.csp.cloud import AzureCloudProvider from atst.domain.csp.cloud.models import ( + AdminRoleDefinitionCSPPayload, + AdminRoleDefinitionCSPResult, BaseCSPPayload, BillingInstructionCSPPayload, BillingInstructionCSPResult, @@ -19,8 +21,18 @@ from atst.domain.csp.cloud.models import ( TaskOrderBillingCreationCSPResult, TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPResult, + TenantAdminOwnershipCSPPayload, + TenantAdminOwnershipCSPResult, TenantCSPPayload, TenantCSPResult, + TenantPrincipalAppCSPPayload, + TenantPrincipalAppCSPResult, + TenantPrincipalCredentialCSPPayload, + TenantPrincipalCredentialCSPResult, + TenantPrincipalCSPPayload, + TenantPrincipalCSPResult, + TenantPrincipalOwnershipCSPPayload, + TenantPrincipalOwnershipCSPResult, ) BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31" @@ -409,46 +421,167 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider): assert body.reported_clin_name == "TO1:CLIN001" -def test_admin_principal_creation(mock_azure: AzureCloudProvider): - # Auth As Tenant Admin - # Create App Registration - # Create Service Principal - # Create App Registration Password Credential - # Lookup global admin role - # Assign global admin role to Service Principal +def test_create_tenant_principal_app(mock_azure: AzureCloudProvider): with patch.object( - AzureCloudProvider, "get_secret", wraps=mock_azure.get_secret - ) as mock_get_secret: - mock_get_secret.return_value = { - "admin_username": "", - "admin_password": "", - } - payload = BaseCSPPayload( + AzureCloudProvider, + "get_elevated_management_token", + wraps=mock_azure.get_elevated_management_token, + ) as get_elevated_management_token: + get_elevated_management_token.return_value = "my fake token" + + mock_result = Mock() + mock_result.ok = True + mock_result.json.return_value = {"appId": "appId", "id": "id"} + + mock_azure.sdk.requests.post.return_value = mock_result + + payload = TenantPrincipalAppCSPPayload( **{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"} ) - result = mock_azure.create_remote_admin(payload) + result: TenantPrincipalAppCSPResult = mock_azure.create_tenant_principal_app( + payload + ) - print(result) + assert result.principal_app_id == "appId" -def test_admin_mg_ownership(mock_azure: AzureCloudProvider): +def test_create_tenant_principal(mock_azure: AzureCloudProvider): with patch.object( - AzureCloudProvider, "get_secret", wraps=mock_azure.get_secret - ) as mock_get_secret: - mock_get_secret.return_value = { - "admin_username": "", - "admin_password": "", - } - payload = TenantCSPResult( + AzureCloudProvider, + "get_elevated_management_token", + wraps=mock_azure.get_elevated_management_token, + ) as get_elevated_management_token: + get_elevated_management_token.return_value = "my fake token" + + mock_result = Mock() + mock_result.ok = True + mock_result.json.return_value = {"id": "principal_id"} + + mock_azure.sdk.requests.post.return_value = mock_result + + payload = TenantPrincipalCSPPayload( + **{ + "tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4", + "principal_app_id": "appId", + } + ) + + result: TenantPrincipalCSPResult = mock_azure.create_tenant_principal(payload) + + assert result.principal_id == "principal_id" + + +def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider): + with patch.object( + AzureCloudProvider, + "get_elevated_management_token", + wraps=mock_azure.get_elevated_management_token, + ) as get_elevated_management_token: + get_elevated_management_token.return_value = "my fake token" + + mock_result = Mock() + mock_result.ok = True + mock_result.json.return_value = {"secretText": "new secret key"} + + mock_azure.sdk.requests.post.return_value = mock_result + + payload = TenantPrincipalCredentialCSPPayload( + **{ + "tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4", + "principal_app_id": "appId", + "principal_app_object_id": "appObjId", + } + ) + + result: TenantPrincipalCredentialCSPResult = mock_azure.create_tenant_principal_credential( + payload + ) + + assert result.principal_secret_key == "new secret key" + + +def test_create_admin_role_definition(mock_azure: AzureCloudProvider): + with patch.object( + AzureCloudProvider, + "get_elevated_management_token", + wraps=mock_azure.get_elevated_management_token, + ) as get_elevated_management_token: + get_elevated_management_token.return_value = "my fake token" + + mock_result = Mock() + mock_result.ok = True + mock_result.json.return_value = { + "value": [ + {"id": "wrongid", "displayName": "Wrong Role"}, + {"id": "id", "displayName": "Company Administrator"}, + ] + } + + mock_azure.sdk.requests.get.return_value = mock_result + + payload = AdminRoleDefinitionCSPPayload( + **{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"} + ) + + result: AdminRoleDefinitionCSPResult = mock_azure.create_admin_role_definition( + payload + ) + + assert result.admin_role_def_id == "id" + + +def test_create_tenant_admin_ownership(mock_azure: AzureCloudProvider): + with patch.object( + AzureCloudProvider, + "get_elevated_management_token", + wraps=mock_azure.get_elevated_management_token, + ) as get_elevated_management_token: + get_elevated_management_token.return_value = "my fake token" + + mock_result = Mock() + mock_result.ok = True + mock_result.json.return_value = {"id": "id"} + + mock_azure.sdk.requests.put.return_value = mock_result + + payload = TenantAdminOwnershipCSPPayload( **{ - "user_id": "blach", "tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4", "user_object_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d", } ) - result = mock_azure.assign_root_mg_ownership(payload) + result: TenantAdminOwnershipCSPResult = mock_azure.create_tenant_admin_ownership( + payload + ) - print(result) + assert result.admin_owner_assignment_id == "id" + +def test_create_tenant_principal_ownership(mock_azure: AzureCloudProvider): + with patch.object( + AzureCloudProvider, + "get_elevated_management_token", + wraps=mock_azure.get_elevated_management_token, + ) as get_elevated_management_token: + get_elevated_management_token.return_value = "my fake token" + + mock_result = Mock() + mock_result.ok = True + mock_result.json.return_value = {"id": "id"} + + mock_azure.sdk.requests.put.return_value = mock_result + + payload = TenantPrincipalOwnershipCSPPayload( + **{ + "tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4", + "principal_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d", + } + ) + + result: TenantPrincipalOwnershipCSPResult = mock_azure.create_tenant_principal_ownership( + payload + ) + + assert result.principal_owner_assignment_id == "id" diff --git a/tests/domain/test_portfolio_state_machine.py b/tests/domain/test_portfolio_state_machine.py index 44d1382b..9480c8a0 100644 --- a/tests/domain/test_portfolio_state_machine.py +++ b/tests/domain/test_portfolio_state_machine.py @@ -104,6 +104,13 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio): FSMStates.TASK_ORDER_BILLING_CREATION_CREATED, FSMStates.TASK_ORDER_BILLING_VERIFICATION_CREATED, FSMStates.BILLING_INSTRUCTION_CREATED, + FSMStates.TENANT_PRINCIPAL_APP_CREATED, + FSMStates.TENANT_PRINCIPAL_CREATED, + FSMStates.TENANT_PRINCIPAL_CREDENTIAL_CREATED, + FSMStates.ADMIN_ROLE_DEFINITION_CREATED, + FSMStates.PRINCIPAL_ADMIN_ROLE_CREATED, + FSMStates.TENANT_ADMIN_OWNERSHIP_CREATED, + FSMStates.TENANT_PRINCIPAL_OWNERSHIP_CREATED, ] if portfolio.csp_data is not None: diff --git a/tests/mock_azure.py b/tests/mock_azure.py index 9963f2d9..6818925e 100644 --- a/tests/mock_azure.py +++ b/tests/mock_azure.py @@ -9,6 +9,9 @@ AZURE_CONFIG = { "AZURE_TENANT_ID": "MOCK", "AZURE_POLICY_LOCATION": "policies", "AZURE_VAULT_URL": "http://vault", + "POWERSHELL_CLIENT_ID": "MOCK", + "AZURE_OWNER_ROLE_DEF_ID": "MOCK", + "AZURE_GRAPH_RESOURCE": "MOCK", } AUTH_CREDENTIALS = { @@ -63,15 +66,13 @@ def mock_policy(): def mock_adal(): import adal - return adal - # return Mock(spec=adal) + return Mock(spec=adal) def mock_requests(): import requests - # return Mock(spec=requests) - return requests + return Mock(spec=requests) def mock_secrets(): From 33c6e8c68c3d4b32c69f59159f3a5011ad396ecd Mon Sep 17 00:00:00 2001 From: tomdds Date: Wed, 29 Jan 2020 18:22:21 -0500 Subject: [PATCH 5/7] Merge CSP secret handling implementations and refine updating. --- atst/domain/csp/cloud/azure_cloud_provider.py | 162 +++++++++--------- atst/domain/csp/cloud/mock_cloud_provider.py | 2 +- atst/domain/csp/cloud/models.py | 5 +- atst/models/portfolio_state_machine.py | 8 - tests/domain/cloud/test_azure_csp.py | 48 +++--- 5 files changed, 111 insertions(+), 114 deletions(-) diff --git a/atst/domain/csp/cloud/azure_cloud_provider.py b/atst/domain/csp/cloud/azure_cloud_provider.py index 735bf53a..7985f363 100644 --- a/atst/domain/csp/cloud/azure_cloud_provider.py +++ b/atst/domain/csp/cloud/azure_cloud_provider.py @@ -1,7 +1,7 @@ import json import re from secrets import token_urlsafe -from typing import Dict +from typing import Any, Dict from uuid import uuid4 from atst.utils import sha256_hex @@ -104,7 +104,7 @@ class AzureCloudProvider(CloudProviderInterface): self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"]) def set_secret(self, secret_key, secret_value): - credential = self._get_client_secret_credential_obj({}) + credential = self._get_client_secret_credential_obj() secret_client = self.sdk.secrets.SecretClient( vault_url=self.vault_url, credential=credential, ) @@ -117,7 +117,7 @@ class AzureCloudProvider(CloudProviderInterface): ) def get_secret(self, secret_key): - credential = self._get_client_secret_credential_obj({}) + credential = self._get_client_secret_credential_obj() secret_client = self.sdk.secrets.SecretClient( vault_url=self.vault_url, credential=credential, ) @@ -318,7 +318,7 @@ class AzureCloudProvider(CloudProviderInterface): ) def create_tenant(self, payload: TenantCSPPayload): - sp_token = self.get_root_provisioning_token() + sp_token = self._get_root_provisioning_token() if sp_token is None: raise AuthenticationException("Could not resolve token for tenant creation") @@ -336,20 +336,27 @@ class AzureCloudProvider(CloudProviderInterface): ) if result.status_code == 200: - return self._ok( - TenantCSPResult( - **result.json(), - tenant_admin_password=payload.password, - tenant_admin_username=payload.user_id, - ) + result_dict = result.json() + tenant_id = result_dict.get("tenantId") + tenant_admin_username = ( + f"{payload.user_id}@{payload.domain_name}.onmicrosoft.com" ) + self.update_tenant_creds( + tenant_id, + KeyVaultCredentials( + tenant_id=tenant_id, + tenant_admin_username=tenant_admin_username, + tenant_admin_password=payload.password, + ), + ) + return self._ok(TenantCSPResult(**result_dict)) else: return self._error(result.json()) def create_billing_profile_creation( self, payload: BillingProfileCreationCSPPayload ): - sp_token = self.get_root_provisioning_token() + sp_token = self._get_root_provisioning_token() if sp_token is None: raise AuthenticationException( "Could not resolve token for billing profile creation" @@ -381,7 +388,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_billing_profile_verification( self, payload: BillingProfileVerificationCSPPayload ): - sp_token = self.get_root_provisioning_token() + sp_token = self._get_root_provisioning_token() if sp_token is None: raise AuthenticationException( "Could not resolve token for billing profile validation" @@ -406,7 +413,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_billing_profile_tenant_access( self, payload: BillingProfileTenantAccessCSPPayload ): - sp_token = self.get_root_provisioning_token() + sp_token = self._get_root_provisioning_token() request_body = { "properties": { "principalTenantId": payload.tenant_id, # from tenant creation @@ -430,7 +437,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_task_order_billing_creation( self, payload: TaskOrderBillingCreationCSPPayload ): - sp_token = self.get_root_provisioning_token() + sp_token = self._get_root_provisioning_token() request_body = [ { "op": "replace", @@ -460,7 +467,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_task_order_billing_verification( self, payload: TaskOrderBillingVerificationCSPPayload ): - sp_token = self.get_root_provisioning_token() + sp_token = self._get_root_provisioning_token() if sp_token is None: raise AuthenticationException( "Could not resolve token for task order billing validation" @@ -483,7 +490,7 @@ class AzureCloudProvider(CloudProviderInterface): return self._error(result.json()) def create_billing_instruction(self, payload: BillingInstructionCSPPayload): - sp_token = self.get_root_provisioning_token() + sp_token = self._get_root_provisioning_token() if sp_token is None: raise AuthenticationException( "Could not resolve token for task order billing validation" @@ -510,28 +517,8 @@ class AzureCloudProvider(CloudProviderInterface): else: return self._error(result.json()) - def get_elevated_management_token(self, tenant_id): - mgmt_token = self.get_tenant_admin_token( - tenant_id, self.sdk.cloud.endpoints.resource_manager - ) - if mgmt_token is None: - raise AuthenticationException( - "Failed to resolve management token for tenant admin" - ) - - auth_header = { - "Authorization": f"Bearer {mgmt_token}", - } - url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01" - result = self.sdk.requests.post(url, headers=auth_header) - - if not result.ok: - raise AuthenticationException("Failed to elevate access") - - return mgmt_token - def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload): - mgmt_token = self.get_elevated_management_token(payload.tenant_id) + mgmt_token = self._get_elevated_management_token(payload.tenant_id) role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}" @@ -558,7 +545,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_tenant_principal_ownership( self, payload: TenantPrincipalOwnershipCSPPayload ): - mgmt_token = self.get_elevated_management_token(payload.tenant_id) + mgmt_token = self._get_elevated_management_token(payload.tenant_id) # NOTE: the tenant_id is also the id of the root management group, once it is created role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}" @@ -584,8 +571,7 @@ class AzureCloudProvider(CloudProviderInterface): return TenantPrincipalOwnershipCSPResult(**response.json()) def create_tenant_principal_app(self, payload: TenantPrincipalAppCSPPayload): - - graph_token = self.get_tenant_admin_token( + graph_token = self._get_tenant_admin_token( payload.tenant_id, self.graph_resource ) if graph_token is None: @@ -607,7 +593,7 @@ class AzureCloudProvider(CloudProviderInterface): return TenantPrincipalAppCSPResult(**response.json()) def create_tenant_principal(self, payload: TenantPrincipalCSPPayload): - graph_token = self.get_tenant_admin_token( + graph_token = self._get_tenant_admin_token( payload.tenant_id, self.graph_resource ) if graph_token is None: @@ -631,7 +617,7 @@ class AzureCloudProvider(CloudProviderInterface): def create_tenant_principal_credential( self, payload: TenantPrincipalCredentialCSPPayload ): - graph_token = self.get_tenant_admin_token( + graph_token = self._get_tenant_admin_token( payload.tenant_id, self.graph_resource ) if graph_token is None: @@ -652,12 +638,22 @@ class AzureCloudProvider(CloudProviderInterface): response = self.sdk.requests.post(url, json=request_body, headers=auth_header) if response.ok: + result = response.json() + self.update_tenant_creds( + payload.tenant_id, + KeyVaultCredentials( + tenant_id=payload.tenant_id, + tenant_sp_key=result.get("secretText"), + tenant_sp_client_id=payload.principal_app_id, + ), + ) return TenantPrincipalCredentialCSPResult( - principal_client_id=payload.principal_app_id, **response.json() + principal_client_id=payload.principal_app_id, + principal_creds_established=True, ) def create_admin_role_definition(self, payload: AdminRoleDefinitionCSPPayload): - graph_token = self.get_tenant_admin_token( + graph_token = self._get_tenant_admin_token( payload.tenant_id, self.graph_resource ) if graph_token is None: @@ -689,7 +685,7 @@ class AzureCloudProvider(CloudProviderInterface): return AdminRoleDefinitionCSPResult(admin_role_def_id=admin_role_def_id) def create_principal_admin_role(self, payload: PrincipalAdminRoleCSPPayload): - graph_token = self.get_tenant_admin_token( + graph_token = self._get_tenant_admin_token( payload.tenant_id, self.graph_resource ) if graph_token is None: @@ -782,33 +778,22 @@ class AzureCloudProvider(CloudProviderInterface): if sub_id_match: return sub_id_match.group(1) - def get_tenant_admin_token(self, tenant_id, resource): - creds = self.get_secret(tenant_id) + def _get_tenant_admin_token(self, tenant_id, resource): + creds = self._source_tenant_creds(tenant_id) return self._get_up_token_for_resource( - creds.get("admin_username"), - creds.get("admin_password"), + creds.tenant_admin_username, + creds.tenant_admin_password, tenant_id, resource, ) - def get_tenant_principal_token(self, tenant_id, resource): - # creds = self.get_secret(tenant_id) - # return self._get_up_token_for_resource( - # creds.get("admin_username"), - # creds.get("admin_password"), - # tenat_id, - # resource - # ) - pass - - def get_root_provisioning_token(self): - return self._get_sp_token(self._root_creds) - - def _get_sp_token(self, creds): - tenant_id = creds.get("tenant_id") - client_id = creds.get("client_id") - secret_key = creds.get("secret_key") + def _get_root_provisioning_token(self): + creds = self._source_creds() + return self._get_sp_token( + creds.tenant_id, creds.root_sp_client_id, creds.root_sp_key + ) + def _get_sp_token(self, tenant_id, client_id, secret_key): context = self.sdk.adal.AuthenticationContext( f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}" ) @@ -842,16 +827,14 @@ class AzureCloudProvider(CloudProviderInterface): cloud_environment=self.sdk.cloud, ) - def _get_client_secret_credential_obj(self, creds): + def _get_client_secret_credential_obj(self): + creds = self._source_creds() return self.sdk.identity.ClientSecretCredential( - tenant_id=creds.get("tenant_id"), - client_id=creds.get("client_id"), - client_secret=creds.get("secret_key"), + tenant_id=creds.tenant_id, + client_id=creds.root_sp_client_id, + client_secret=creds.root_sp_key, ) - def _make_tenant_admin_cred_obj(self, username, password): - return self.sdk.credentials.UserPassCredentials(username, password) - def _ok(self, body=None): return self._make_response("ok", body) @@ -878,6 +861,26 @@ class AzureCloudProvider(CloudProviderInterface): "tenant_id": self.tenant_id, } + def _get_elevated_management_token(self, tenant_id): + mgmt_token = self._get_tenant_admin_token( + tenant_id, self.sdk.cloud.endpoints.resource_manager + ) + if mgmt_token is None: + raise AuthenticationException( + "Failed to resolve management token for tenant admin" + ) + + auth_header = { + "Authorization": f"Bearer {mgmt_token}", + } + url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01" + result = self.sdk.requests.post(url, headers=auth_header) + + if not result.ok: + raise AuthenticationException("Failed to elevate access") + + return mgmt_token + def _source_creds(self, tenant_id=None) -> KeyVaultCredentials: if tenant_id: return self._source_tenant_creds(tenant_id) @@ -888,13 +891,16 @@ class AzureCloudProvider(CloudProviderInterface): root_sp_key=self._root_creds.get("secret_key"), ) - def update_tenant_creds(self, tenant_id, secret): + def update_tenant_creds(self, tenant_id, secret: KeyVaultCredentials): hashed = sha256_hex(tenant_id) - self.set_secret(hashed, json.dumps(secret)) + new_secrets = secret.dict() + curr_secrets = self._source_tenant_creds(tenant_id) + updated_secrets: Dict[str, Any] = {**curr_secrets.dict(), **new_secrets} + us = KeyVaultCredentials(**updated_secrets) + self.set_secret(hashed, json.dumps(us.dict())) + return us - return secret - - def _source_tenant_creds(self, tenant_id): + def _source_tenant_creds(self, tenant_id) -> KeyVaultCredentials: hashed = sha256_hex(tenant_id) raw_creds = self.get_secret(hashed) return KeyVaultCredentials(**json.loads(raw_creds)) diff --git a/atst/domain/csp/cloud/mock_cloud_provider.py b/atst/domain/csp/cloud/mock_cloud_provider.py index 52e68f08..fcc9495a 100644 --- a/atst/domain/csp/cloud/mock_cloud_provider.py +++ b/atst/domain/csp/cloud/mock_cloud_provider.py @@ -331,8 +331,8 @@ class MockCloudProvider(CloudProviderInterface): return TenantPrincipalCredentialCSPResult( **dict( - secretText="principal_secret_key", principal_client_id="principal_client_id", + principal_creds_established=True, ) ) diff --git a/atst/domain/csp/cloud/models.py b/atst/domain/csp/cloud/models.py index ce7769a1..18c969d6 100644 --- a/atst/domain/csp/cloud/models.py +++ b/atst/domain/csp/cloud/models.py @@ -278,10 +278,7 @@ class TenantPrincipalCredentialCSPPayload(BaseCSPPayload): class TenantPrincipalCredentialCSPResult(AliasModel): principal_client_id: str - principal_secret_key: str - - class Config: - fields = {"principal_secret_key": "secretText"} + principal_creds_established: bool class AdminRoleDefinitionCSPPayload(BaseCSPPayload): diff --git a/atst/models/portfolio_state_machine.py b/atst/models/portfolio_state_machine.py index be9324b1..4b14a087 100644 --- a/atst/models/portfolio_state_machine.py +++ b/atst/models/portfolio_state_machine.py @@ -168,14 +168,6 @@ class PortfolioStateMachine( self.portfolio.csp_data.update(response.dict()) db.session.add(self.portfolio) db.session.commit() - - if getattr(response, "get_creds", None) is not None: - new_creds = response.get_creds() - # TODO: one way salted hash of tenant_id to use as kv key name? - tenant_id = new_creds.get("tenant_id") - secret = self.csp.get_secret(tenant_id, new_creds) - secret.update(new_creds) - self.csp.update_tenant_creds(tenant_id, secret) except PydanticValidationError as exc: app.logger.error( f"Failed to cast response to valid result class {self.__repr__()}:", diff --git a/tests/domain/cloud/test_azure_csp.py b/tests/domain/cloud/test_azure_csp.py index ac5f62a2..67c03c9f 100644 --- a/tests/domain/cloud/test_azure_csp.py +++ b/tests/domain/cloud/test_azure_csp.py @@ -102,8 +102,10 @@ MOCK_CREDS = { } -def mock_get_secret(azure, func): - azure.get_secret = func +def mock_get_secret(azure, val=None): + if val is None: + val = json.dumps(MOCK_CREDS) + azure.get_secret = lambda *a, **k: val return azure @@ -111,12 +113,12 @@ def mock_get_secret(azure, func): def test_create_application_succeeds(mock_azure: AzureCloudProvider): application = ApplicationFactory.create() mock_management_group_create(mock_azure, {"id": "Test Id"}) - - mock_azure = mock_get_secret(mock_azure, lambda *a, **k: json.dumps(MOCK_CREDS)) + mock_azure = mock_get_secret(mock_azure) payload = ApplicationCSPPayload( tenant_id="1234", display_name=application.name, parent_id=str(uuid4()) ) + result = mock_azure.create_application(payload) assert result.id == "Test Id" @@ -162,10 +164,6 @@ def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider): 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", @@ -176,7 +174,6 @@ def test_create_tenant(mock_azure: AzureCloudProvider): mock_azure.sdk.requests.post.return_value = mock_result payload = TenantCSPPayload( **dict( - tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", user_id="admin", password="JediJan13$coot", # pragma: allowlist secret domain_name="jediccpospawnedtenant2", @@ -186,6 +183,7 @@ def test_create_tenant(mock_azure: AzureCloudProvider): password_recovery_email_address="thomas@promptworks.com", ) ) + mock_azure = mock_get_secret(mock_azure) result = mock_azure.create_tenant(payload) body: TenantCSPResult = result.get("body") assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435" @@ -446,8 +444,8 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider): def test_create_tenant_principal_app(mock_azure: AzureCloudProvider): with patch.object( AzureCloudProvider, - "get_elevated_management_token", - wraps=mock_azure.get_elevated_management_token, + "_get_elevated_management_token", + wraps=mock_azure._get_elevated_management_token, ) as get_elevated_management_token: get_elevated_management_token.return_value = "my fake token" @@ -456,11 +454,11 @@ def test_create_tenant_principal_app(mock_azure: AzureCloudProvider): mock_result.json.return_value = {"appId": "appId", "id": "id"} mock_azure.sdk.requests.post.return_value = mock_result + mock_azure = mock_get_secret(mock_azure) payload = TenantPrincipalAppCSPPayload( **{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"} ) - result: TenantPrincipalAppCSPResult = mock_azure.create_tenant_principal_app( payload ) @@ -471,8 +469,8 @@ def test_create_tenant_principal_app(mock_azure: AzureCloudProvider): def test_create_tenant_principal(mock_azure: AzureCloudProvider): with patch.object( AzureCloudProvider, - "get_elevated_management_token", - wraps=mock_azure.get_elevated_management_token, + "_get_elevated_management_token", + wraps=mock_azure._get_elevated_management_token, ) as get_elevated_management_token: get_elevated_management_token.return_value = "my fake token" @@ -481,6 +479,7 @@ def test_create_tenant_principal(mock_azure: AzureCloudProvider): mock_result.json.return_value = {"id": "principal_id"} mock_azure.sdk.requests.post.return_value = mock_result + mock_azure = mock_get_secret(mock_azure) payload = TenantPrincipalCSPPayload( **{ @@ -497,8 +496,8 @@ def test_create_tenant_principal(mock_azure: AzureCloudProvider): def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider): with patch.object( AzureCloudProvider, - "get_elevated_management_token", - wraps=mock_azure.get_elevated_management_token, + "_get_elevated_management_token", + wraps=mock_azure._get_elevated_management_token, ) as get_elevated_management_token: get_elevated_management_token.return_value = "my fake token" @@ -508,6 +507,8 @@ def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider): mock_azure.sdk.requests.post.return_value = mock_result + mock_azure = mock_get_secret(mock_azure) + payload = TenantPrincipalCredentialCSPPayload( **{ "tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4", @@ -520,14 +521,14 @@ def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider): payload ) - assert result.principal_secret_key == "new secret key" + assert result.principal_creds_established == True def test_create_admin_role_definition(mock_azure: AzureCloudProvider): with patch.object( AzureCloudProvider, - "get_elevated_management_token", - wraps=mock_azure.get_elevated_management_token, + "_get_elevated_management_token", + wraps=mock_azure._get_elevated_management_token, ) as get_elevated_management_token: get_elevated_management_token.return_value = "my fake token" @@ -541,6 +542,7 @@ def test_create_admin_role_definition(mock_azure: AzureCloudProvider): } mock_azure.sdk.requests.get.return_value = mock_result + mock_azure = mock_get_secret(mock_azure) payload = AdminRoleDefinitionCSPPayload( **{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"} @@ -556,8 +558,8 @@ def test_create_admin_role_definition(mock_azure: AzureCloudProvider): def test_create_tenant_admin_ownership(mock_azure: AzureCloudProvider): with patch.object( AzureCloudProvider, - "get_elevated_management_token", - wraps=mock_azure.get_elevated_management_token, + "_get_elevated_management_token", + wraps=mock_azure._get_elevated_management_token, ) as get_elevated_management_token: get_elevated_management_token.return_value = "my fake token" @@ -584,8 +586,8 @@ def test_create_tenant_admin_ownership(mock_azure: AzureCloudProvider): def test_create_tenant_principal_ownership(mock_azure: AzureCloudProvider): with patch.object( AzureCloudProvider, - "get_elevated_management_token", - wraps=mock_azure.get_elevated_management_token, + "_get_elevated_management_token", + wraps=mock_azure._get_elevated_management_token, ) as get_elevated_management_token: get_elevated_management_token.return_value = "my fake token" From 295abf49f1bbc5ededd72c0ab7fcac88eb3bf0ed Mon Sep 17 00:00:00 2001 From: tomdds Date: Thu, 30 Jan 2020 10:44:08 -0500 Subject: [PATCH 6/7] Add new Portfolio Step items to DB Enum --- ..._admin_and_ownership_provisioning_steps.py | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 alembic/versions/cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py diff --git a/alembic/versions/cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py b/alembic/versions/cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py new file mode 100644 index 00000000..26c2d9e6 --- /dev/null +++ b/alembic/versions/cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py @@ -0,0 +1,199 @@ +"""Admin and Ownership Provisioning Steps + +Revision ID: cd7e3f9a5d64 +Revises: 508957112ed6 +Create Date: 2020-01-30 10:38:04.182953 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "cd7e3f9a5d64" # pragma: allowlist secret +down_revision = "508957112ed6" # pragma: allowlist secret +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "portfolio_state_machines", + "state", + type_=sa.Enum( + "UNSTARTED", + "STARTING", + "STARTED", + "COMPLETED", + "FAILED", + "TENANT_CREATED", + "TENANT_IN_PROGRESS", + "TENANT_FAILED", + "BILLING_PROFILE_CREATION_CREATED", + "BILLING_PROFILE_CREATION_IN_PROGRESS", + "BILLING_PROFILE_CREATION_FAILED", + "BILLING_PROFILE_VERIFICATION_CREATED", + "BILLING_PROFILE_VERIFICATION_IN_PROGRESS", + "BILLING_PROFILE_VERIFICATION_FAILED", + "BILLING_PROFILE_TENANT_ACCESS_CREATED", + "BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS", + "BILLING_PROFILE_TENANT_ACCESS_FAILED", + "TASK_ORDER_BILLING_CREATION_CREATED", + "TASK_ORDER_BILLING_CREATION_IN_PROGRESS", + "TASK_ORDER_BILLING_CREATION_FAILED", + "TASK_ORDER_BILLING_VERIFICATION_CREATED", + "TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS", + "TASK_ORDER_BILLING_VERIFICATION_FAILED", + "BILLING_INSTRUCTION_CREATED", + "BILLING_INSTRUCTION_IN_PROGRESS", + "BILLING_INSTRUCTION_FAILED", + "TENANT_PRINCIPAL_APP_CREATED", + "TENANT_PRINCIPAL_APP_IN_PROGRESS", + "TENANT_PRINCIPAL_APP_FAILED", + "TENANT_PRINCIPAL_CREATED", + "TENANT_PRINCIPAL_IN_PROGRESS", + "TENANT_PRINCIPAL_FAILED", + "TENANT_PRINCIPAL_CREDENTIAL_CREATED", + "TENANT_PRINCIPAL_CREDENTIAL_IN_PROGRESS", + "TENANT_PRINCIPAL_CREDENTIAL_FAILED", + "ADMIN_ROLE_DEFINITION_CREATED", + "ADMIN_ROLE_DEFINITION_IN_PROGRESS", + "ADMIN_ROLE_DEFINITION_FAILED", + "PRINCIPAL_ADMIN_ROLE_CREATED", + "PRINCIPAL_ADMIN_ROLE_IN_PROGRESS", + "PRINCIPAL_ADMIN_ROLE_FAILED", + "TENANT_ADMIN_OWNERSHIP_CREATED", + "TENANT_ADMIN_OWNERSHIP_IN_PROGRESS", + "TENANT_ADMIN_OWNERSHIP_FAILED", + "TENANT_PRINCIPAL_OWNERSHIP_CREATED", + "TENANT_PRINCIPAL_OWNERSHIP_IN_PROGRESS", + "TENANT_PRINCIPAL_OWNERSHIP_FAILED", + name="fsmstates", + native_enum=False, + ), + existing_type=sa.Enum( + "UNSTARTED", + "STARTING", + "STARTED", + "COMPLETED", + "FAILED", + "TENANT_CREATED", + "TENANT_IN_PROGRESS", + "TENANT_FAILED", + "BILLING_PROFILE_CREATION_CREATED", + "BILLING_PROFILE_CREATION_IN_PROGRESS", + "BILLING_PROFILE_CREATION_FAILED", + "BILLING_PROFILE_VERIFICATION_CREATED", + "BILLING_PROFILE_VERIFICATION_IN_PROGRESS", + "BILLING_PROFILE_VERIFICATION_FAILED", + "BILLING_PROFILE_TENANT_ACCESS_CREATED", + "BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS", + "BILLING_PROFILE_TENANT_ACCESS_FAILED", + "TASK_ORDER_BILLING_CREATION_CREATED", + "TASK_ORDER_BILLING_CREATION_IN_PROGRESS", + "TASK_ORDER_BILLING_CREATION_FAILED", + "TASK_ORDER_BILLING_VERIFICATION_CREATED", + "TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS", + "TASK_ORDER_BILLING_VERIFICATION_FAILED", + "BILLING_INSTRUCTION_CREATED", + "BILLING_INSTRUCTION_IN_PROGRESS", + "BILLING_INSTRUCTION_FAILED", + name="fsmstates", + native_enum=False, + create_constraint=False, + ), + existing_nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "portfolio_state_machines", + "state", + type_=sa.Enum( + "UNSTARTED", + "STARTING", + "STARTED", + "COMPLETED", + "FAILED", + "TENANT_CREATED", + "TENANT_IN_PROGRESS", + "TENANT_FAILED", + "BILLING_PROFILE_CREATION_CREATED", + "BILLING_PROFILE_CREATION_IN_PROGRESS", + "BILLING_PROFILE_CREATION_FAILED", + "BILLING_PROFILE_VERIFICATION_CREATED", + "BILLING_PROFILE_VERIFICATION_IN_PROGRESS", + "BILLING_PROFILE_VERIFICATION_FAILED", + "BILLING_PROFILE_TENANT_ACCESS_CREATED", + "BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS", + "BILLING_PROFILE_TENANT_ACCESS_FAILED", + "TASK_ORDER_BILLING_CREATION_CREATED", + "TASK_ORDER_BILLING_CREATION_IN_PROGRESS", + "TASK_ORDER_BILLING_CREATION_FAILED", + "TASK_ORDER_BILLING_VERIFICATION_CREATED", + "TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS", + "TASK_ORDER_BILLING_VERIFICATION_FAILED", + "BILLING_INSTRUCTION_CREATED", + "BILLING_INSTRUCTION_IN_PROGRESS", + "BILLING_INSTRUCTION_FAILED", + name="fsmstates", + native_enum=False, + ), + existing_type=sa.Enum( + "UNSTARTED", + "STARTING", + "STARTED", + "COMPLETED", + "FAILED", + "TENANT_CREATED", + "TENANT_IN_PROGRESS", + "TENANT_FAILED", + "BILLING_PROFILE_CREATION_CREATED", + "BILLING_PROFILE_CREATION_IN_PROGRESS", + "BILLING_PROFILE_CREATION_FAILED", + "BILLING_PROFILE_VERIFICATION_CREATED", + "BILLING_PROFILE_VERIFICATION_IN_PROGRESS", + "BILLING_PROFILE_VERIFICATION_FAILED", + "BILLING_PROFILE_TENANT_ACCESS_CREATED", + "BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS", + "BILLING_PROFILE_TENANT_ACCESS_FAILED", + "TASK_ORDER_BILLING_CREATION_CREATED", + "TASK_ORDER_BILLING_CREATION_IN_PROGRESS", + "TASK_ORDER_BILLING_CREATION_FAILED", + "TASK_ORDER_BILLING_VERIFICATION_CREATED", + "TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS", + "TASK_ORDER_BILLING_VERIFICATION_FAILED", + "BILLING_INSTRUCTION_CREATED", + "BILLING_INSTRUCTION_IN_PROGRESS", + "BILLING_INSTRUCTION_FAILED", + "TENANT_PRINCIPAL_APP_CREATED", + "TENANT_PRINCIPAL_APP_IN_PROGRESS", + "TENANT_PRINCIPAL_APP_FAILED", + "TENANT_PRINCIPAL_CREATED", + "TENANT_PRINCIPAL_IN_PROGRESS", + "TENANT_PRINCIPAL_FAILED", + "TENANT_PRINCIPAL_CREDENTIAL_CREATED", + "TENANT_PRINCIPAL_CREDENTIAL_IN_PROGRESS", + "TENANT_PRINCIPAL_CREDENTIAL_FAILED", + "ADMIN_ROLE_DEFINITION_CREATED", + "ADMIN_ROLE_DEFINITION_IN_PROGRESS", + "ADMIN_ROLE_DEFINITION_FAILED", + "PRINCIPAL_ADMIN_ROLE_CREATED", + "PRINCIPAL_ADMIN_ROLE_IN_PROGRESS", + "PRINCIPAL_ADMIN_ROLE_FAILED", + "TENANT_ADMIN_OWNERSHIP_CREATED", + "TENANT_ADMIN_OWNERSHIP_IN_PROGRESS", + "TENANT_ADMIN_OWNERSHIP_FAILED", + "TENANT_PRINCIPAL_OWNERSHIP_CREATED", + "TENANT_PRINCIPAL_OWNERSHIP_IN_PROGRESS", + "TENANT_PRINCIPAL_OWNERSHIP_FAILED", + name="fsmstates", + native_enum=False, + ), + existing_nullable=False, + ) + # ### end Alembic commands ### From f6d3090177fefa63ebc70c09533153afb1008c37 Mon Sep 17 00:00:00 2001 From: tomdds Date: Thu, 30 Jan 2020 11:05:06 -0500 Subject: [PATCH 7/7] Remove unused postgres import in migration --- .../cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/alembic/versions/cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py b/alembic/versions/cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py index 26c2d9e6..08f16c3f 100644 --- a/alembic/versions/cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py +++ b/alembic/versions/cd7e3f9a5d64_admin_and_ownership_provisioning_steps.py @@ -7,7 +7,6 @@ Create Date: 2020-01-30 10:38:04.182953 """ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision = "cd7e3f9a5d64" # pragma: allowlist secret