Merge pull request #1372 from dod-ccpo/azure-admin-provisioning

Azure Admin Provisioning
This commit is contained in:
tomdds 2020-01-30 11:47:49 -05:00 committed by GitHub
commit 001d6cbeda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 890 additions and 124 deletions

View File

@ -0,0 +1,198 @@
"""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
# 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 ###

View File

@ -1,12 +1,16 @@
import json import json
import re import re
from secrets import token_urlsafe from secrets import token_urlsafe
from typing import Dict from typing import Any, Dict
from uuid import uuid4 from uuid import uuid4
from atst.utils import sha256_hex
from .cloud_provider_interface import CloudProviderInterface from .cloud_provider_interface import CloudProviderInterface
from .exceptions import AuthenticationException from .exceptions import AuthenticationException
from .models import ( from .models import (
AdminRoleDefinitionCSPPayload,
AdminRoleDefinitionCSPResult,
ApplicationCSPPayload, ApplicationCSPPayload,
ApplicationCSPResult, ApplicationCSPResult,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
@ -19,26 +23,36 @@ from .models import (
BillingProfileVerificationCSPResult, BillingProfileVerificationCSPResult,
KeyVaultCredentials, KeyVaultCredentials,
ManagementGroupCSPResponse, ManagementGroupCSPResponse,
PrincipalAdminRoleCSPPayload,
PrincipalAdminRoleCSPResult,
TaskOrderBillingCreationCSPPayload, TaskOrderBillingCreationCSPPayload,
TaskOrderBillingCreationCSPResult, TaskOrderBillingCreationCSPResult,
TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPPayload,
TaskOrderBillingVerificationCSPResult, TaskOrderBillingVerificationCSPResult,
TenantAdminOwnershipCSPPayload,
TenantAdminOwnershipCSPResult,
TenantCSPPayload, TenantCSPPayload,
TenantCSPResult, TenantCSPResult,
TenantPrincipalAppCSPPayload,
TenantPrincipalAppCSPResult,
TenantPrincipalCredentialCSPPayload,
TenantPrincipalCredentialCSPResult,
TenantPrincipalCSPPayload,
TenantPrincipalCSPResult,
TenantPrincipalOwnershipCSPPayload,
TenantPrincipalOwnershipCSPResult,
) )
from .policy import AzurePolicyManager from .policy import AzurePolicyManager
from atst.utils import sha256_hex
AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD
AZURE_SKU_ID = "?" # probably a static sku specific to ATAT/JEDI
SUBSCRIPTION_ID_REGEX = re.compile( 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})", "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, re.I,
) )
# This needs to be a fully pathed role definition identifier, not just a UUID # 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" REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00000000-0000-4000-8000-000000000000"
AZURE_MANAGEMENT_API = "https://management.azure.com"
class AzureSDKProvider(object): class AzureSDKProvider(object):
@ -50,8 +64,9 @@ class AzureSDKProvider(object):
import azure.identity as identity import azure.identity as identity
from azure.keyvault import secrets from azure.keyvault import secrets
from azure.core import exceptions from azure.core import exceptions
from msrestazure.azure_cloud import (
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD AZURE_PUBLIC_CLOUD,
) # TODO: choose cloud type from config
import adal import adal
import requests import requests
@ -66,7 +81,6 @@ class AzureSDKProvider(object):
self.exceptions = exceptions self.exceptions = exceptions
self.secrets = secrets self.secrets = secrets
self.requests = requests self.requests = requests
# may change to a JEDI cloud
self.cloud = AZURE_PUBLIC_CLOUD self.cloud = AZURE_PUBLIC_CLOUD
@ -78,6 +92,9 @@ class AzureCloudProvider(CloudProviderInterface):
self.secret_key = config["AZURE_SECRET_KEY"] self.secret_key = config["AZURE_SECRET_KEY"]
self.tenant_id = config["AZURE_TENANT_ID"] self.tenant_id = config["AZURE_TENANT_ID"]
self.vault_url = config["AZURE_VAULT_URL"] self.vault_url = config["AZURE_VAULT_URL"]
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: if azure_sdk_provider is None:
self.sdk = AzureSDKProvider() self.sdk = AzureSDKProvider()
@ -87,7 +104,7 @@ class AzureCloudProvider(CloudProviderInterface):
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"]) self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
def set_secret(self, secret_key, secret_value): 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( secret_client = self.sdk.secrets.SecretClient(
vault_url=self.vault_url, credential=credential, vault_url=self.vault_url, credential=credential,
) )
@ -100,7 +117,7 @@ class AzureCloudProvider(CloudProviderInterface):
) )
def get_secret(self, secret_key): 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( secret_client = self.sdk.secrets.SecretClient(
vault_url=self.vault_url, credential=credential, vault_url=self.vault_url, credential=credential,
) )
@ -176,7 +193,7 @@ class AzureCloudProvider(CloudProviderInterface):
"secret_key": creds.root_sp_key, "secret_key": creds.root_sp_key,
"tenant_id": creds.root_tenant_id, "tenant_id": creds.root_tenant_id,
}, },
resource=AZURE_MANAGEMENT_API, resource=self.sdk.cloud.endpoints.resource_manager,
) )
response = self._create_management_group( response = self._create_management_group(
@ -301,7 +318,7 @@ class AzureCloudProvider(CloudProviderInterface):
) )
def create_tenant(self, payload: TenantCSPPayload): 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: if sp_token is None:
raise AuthenticationException("Could not resolve token for tenant creation") raise AuthenticationException("Could not resolve token for tenant creation")
@ -313,26 +330,33 @@ class AzureCloudProvider(CloudProviderInterface):
} }
result = self.sdk.requests.post( 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, json=create_tenant_body,
headers=create_tenant_headers, headers=create_tenant_headers,
) )
if result.status_code == 200: if result.status_code == 200:
return self._ok( result_dict = result.json()
TenantCSPResult( tenant_id = result_dict.get("tenantId")
**result.json(), tenant_admin_username = (
tenant_admin_password=payload.password, f"{payload.user_id}@{payload.domain_name}.onmicrosoft.com"
tenant_admin_username=payload.user_id,
)
) )
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: else:
return self._error(result.json()) return self._error(result.json())
def create_billing_profile_creation( def create_billing_profile_creation(
self, payload: BillingProfileCreationCSPPayload self, payload: BillingProfileCreationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for billing profile creation" "Could not resolve token for billing profile creation"
@ -344,7 +368,7 @@ class AzureCloudProvider(CloudProviderInterface):
"Authorization": f"Bearer {sp_token}", "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( result = self.sdk.requests.post(
billing_account_create_url, billing_account_create_url,
@ -364,7 +388,7 @@ class AzureCloudProvider(CloudProviderInterface):
def create_billing_profile_verification( def create_billing_profile_verification(
self, payload: BillingProfileVerificationCSPPayload self, payload: BillingProfileVerificationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for billing profile validation" "Could not resolve token for billing profile validation"
@ -389,7 +413,7 @@ class AzureCloudProvider(CloudProviderInterface):
def create_billing_profile_tenant_access( def create_billing_profile_tenant_access(
self, payload: BillingProfileTenantAccessCSPPayload self, payload: BillingProfileTenantAccessCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
request_body = { request_body = {
"properties": { "properties": {
"principalTenantId": payload.tenant_id, # from tenant creation "principalTenantId": payload.tenant_id, # from tenant creation
@ -402,7 +426,7 @@ class AzureCloudProvider(CloudProviderInterface):
"Authorization": f"Bearer {sp_token}", "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) result = self.sdk.requests.post(url, headers=headers, json=request_body)
if result.status_code == 201: if result.status_code == 201:
@ -413,12 +437,12 @@ class AzureCloudProvider(CloudProviderInterface):
def create_task_order_billing_creation( def create_task_order_billing_creation(
self, payload: TaskOrderBillingCreationCSPPayload self, payload: TaskOrderBillingCreationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
request_body = [ request_body = [
{ {
"op": "replace", "op": "replace",
"path": "/enabledAzurePlans", "path": "/enabledAzurePlans",
"value": [{"skuId": "0001"}], "value": [{"skuId": AZURE_SKU_ID}],
} }
] ]
@ -426,7 +450,7 @@ class AzureCloudProvider(CloudProviderInterface):
"Authorization": f"Bearer {sp_token}", "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( result = self.sdk.requests.patch(
url, headers=request_headers, json=request_body url, headers=request_headers, json=request_body
@ -443,7 +467,7 @@ class AzureCloudProvider(CloudProviderInterface):
def create_task_order_billing_verification( def create_task_order_billing_verification(
self, payload: TaskOrderBillingVerificationCSPPayload self, payload: TaskOrderBillingVerificationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for task order billing validation" "Could not resolve token for task order billing validation"
@ -466,7 +490,7 @@ class AzureCloudProvider(CloudProviderInterface):
return self._error(result.json()) return self._error(result.json())
def create_billing_instruction(self, payload: BillingInstructionCSPPayload): 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: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for task order billing validation" "Could not resolve token for task order billing validation"
@ -480,7 +504,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 = { auth_header = {
"Authorization": f"Bearer {sp_token}", "Authorization": f"Bearer {sp_token}",
@ -493,21 +517,198 @@ class AzureCloudProvider(CloudProviderInterface):
else: else:
return self._error(result.json()) return self._error(result.json())
def create_remote_admin(self, creds, tenant_details): def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload):
# create app/service principal within tenant, with name constructed from tenant details mgmt_token = self._get_elevated_management_token(payload.tenant_id)
# assign principal global admin
# needs to call out to CLI with tenant owner username/password, prototyping for that underway role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}"
# return identifier and creds to consumer for storage request_body = {
response = {"clientId": "string", "secretKey": "string", "tenantId": "string"} "properties": {
return self._ok( "roleDefinitionId": role_definition_id,
{ "principalId": payload.user_object_id,
"client_id": response["clientId"],
"secret_key": response["secret_key"],
"tenant_id": response["tenantId"],
} }
}
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 response.ok:
return TenantAdminOwnershipCSPResult(**response.json())
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/{self.owner_role_def_id}"
request_body = {
"properties": {
"roleDefinitionId": role_definition_id,
"principalId": payload.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 response.ok:
return TenantPrincipalOwnershipCSPResult(**response.json())
def create_tenant_principal_app(self, payload: TenantPrincipalAppCSPPayload):
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 = {"displayName": "ATAT Remote Admin"}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/v1.0/applications"
response = self.sdk.requests.post(url, json=request_body, headers=auth_header)
if response.ok:
return TenantPrincipalAppCSPResult(**response.json())
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": payload.principal_app_id}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/beta/servicePrincipals"
response = self.sdk.requests.post(url, json=request_body, headers=auth_header)
if response.ok:
return TenantPrincipalCSPResult(**response.json())
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"}]
}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
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)
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,
principal_creds_established=True,
)
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}",
}
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")
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,
)
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": payload.principal_id,
"roleDefinitionId": payload.admin_role_def_id,
"resourceScope": "/",
}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
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 PrincipalAdminRoleCSPResult(**response.json())
def force_tenant_admin_pw_update(self, creds, tenant_owner_id): def force_tenant_admin_pw_update(self, creds, tenant_owner_id):
# use creds to update to force password recovery? # use creds to update to force password recovery?
@ -577,22 +778,42 @@ class AzureCloudProvider(CloudProviderInterface):
if sub_id_match: if sub_id_match:
return sub_id_match.group(1) return sub_id_match.group(1)
def _get_sp_token(self, creds): def _get_tenant_admin_token(self, tenant_id, resource):
home_tenant_id = creds.get("home_tenant_id") creds = self._source_tenant_creds(tenant_id)
client_id = creds.get("client_id") return self._get_up_token_for_resource(
secret_key = creds.get("secret_key") creds.tenant_admin_username,
creds.tenant_admin_password,
tenant_id,
resource,
)
# TODO: Make endpoints consts or configs def _get_root_provisioning_token(self):
authentication_endpoint = "https://login.microsoftonline.com/" creds = self._source_creds()
resource = "https://management.azure.com/" 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( context = self.sdk.adal.AuthenticationContext(
authentication_endpoint + home_tenant_id f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
) )
# TODO: handle failure states here # TODO: handle failure states here
token_response = context.acquire_token_with_client_credentials( 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)
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) return token_response.get("accessToken", None)
@ -606,16 +827,14 @@ class AzureCloudProvider(CloudProviderInterface):
cloud_environment=self.sdk.cloud, 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( return self.sdk.identity.ClientSecretCredential(
tenant_id=creds.get("tenant_id"), tenant_id=creds.tenant_id,
client_id=creds.get("client_id"), client_id=creds.root_sp_client_id,
client_secret=creds.get("secret_key"), 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): def _ok(self, body=None):
return self._make_response("ok", body) return self._make_response("ok", body)
@ -642,6 +861,26 @@ class AzureCloudProvider(CloudProviderInterface):
"tenant_id": self.tenant_id, "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: def _source_creds(self, tenant_id=None) -> KeyVaultCredentials:
if tenant_id: if tenant_id:
return self._source_tenant_creds(tenant_id) return self._source_tenant_creds(tenant_id)
@ -652,13 +891,16 @@ class AzureCloudProvider(CloudProviderInterface):
root_sp_key=self._root_creds.get("secret_key"), 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) 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) -> KeyVaultCredentials:
def _source_tenant_creds(self, tenant_id):
hashed = sha256_hex(tenant_id) hashed = sha256_hex(tenant_id)
raw_creds = self.get_secret(hashed) raw_creds = self.get_secret(hashed)
return KeyVaultCredentials(**json.loads(raw_creds)) return KeyVaultCredentials(**json.loads(raw_creds))

View File

@ -1,37 +1,48 @@
from uuid import uuid4 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 .cloud_provider_interface import CloudProviderInterface
from .exceptions import ( from .exceptions import (
AuthenticationException, AuthenticationException,
AuthorizationException, AuthorizationException,
BaselineProvisionException,
ConnectionException, ConnectionException,
EnvironmentCreationException,
GeneralCSPException,
UnknownServerException, UnknownServerException,
UserProvisioningException,
UserRemovalException,
) )
from .models import ( from .models import (
AZURE_MGMNT_PATH, AZURE_MGMNT_PATH,
AdminRoleDefinitionCSPPayload,
AdminRoleDefinitionCSPResult,
ApplicationCSPPayload, ApplicationCSPPayload,
ApplicationCSPResult, ApplicationCSPResult,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
BillingInstructionCSPResult, BillingInstructionCSPResult,
BillingProfileCreationCSPPayload, BillingProfileCreationCSPPayload,
BillingProfileCreationCSPResult, BillingProfileCreationCSPResult,
BillingProfileTenantAccessCSPResult,
BillingProfileVerificationCSPPayload, BillingProfileVerificationCSPPayload,
BillingProfileVerificationCSPResult, BillingProfileVerificationCSPResult,
PrincipalAdminRoleCSPPayload,
PrincipalAdminRoleCSPResult,
TaskOrderBillingCreationCSPPayload, TaskOrderBillingCreationCSPPayload,
TaskOrderBillingCreationCSPResult, TaskOrderBillingCreationCSPResult,
TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPPayload,
TaskOrderBillingVerificationCSPResult, TaskOrderBillingVerificationCSPResult,
TenantAdminOwnershipCSPPayload,
TenantAdminOwnershipCSPResult,
TenantCSPPayload, TenantCSPPayload,
TenantCSPResult, TenantCSPResult,
TenantPrincipalAppCSPPayload,
TenantPrincipalAppCSPResult,
TenantPrincipalCredentialCSPPayload,
TenantPrincipalCredentialCSPResult,
TenantPrincipalCSPPayload,
TenantPrincipalCSPResult,
TenantPrincipalOwnershipCSPPayload,
TenantPrincipalOwnershipCSPResult,
) )
@ -120,7 +131,7 @@ class MockCloudProvider(CloudProviderInterface):
payload is an instance of TenantCSPPayload data class payload is an instance of TenantCSPPayload data class
""" """
self._authorize(payload.creds) self._authorize("admin")
self._delay(1, 5) self._delay(1, 5)
@ -277,6 +288,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(
principal_client_id="principal_client_id",
principal_creds_established=True,
)
)
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): def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
self._authorize(auth_credentials) self._authorize(auth_credentials)

View File

@ -22,20 +22,10 @@ class AliasModel(BaseModel):
class BaseCSPPayload(AliasModel): class BaseCSPPayload(AliasModel):
# {"username": "mock-cloud", "pass": "shh"} tenant_id: str
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)
class TenantCSPPayload(BaseCSPPayload): class TenantCSPPayload(AliasModel):
user_id: str user_id: str
password: Optional[str] password: Optional[str]
domain_name: str domain_name: str
@ -236,6 +226,81 @@ class BillingInstructionCSPResult(AliasModel):
} }
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_creds_established: bool
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"}
AZURE_MGMNT_PATH = "/providers/Microsoft.Management/managementGroups/" AZURE_MGMNT_PATH = "/providers/Microsoft.Management/managementGroups/"
MANAGEMENT_GROUP_NAME_REGEX = "^[a-zA-Z0-9\-_\(\)\.]+$" MANAGEMENT_GROUP_NAME_REGEX = "^[a-zA-Z0-9\-_\(\)\.]+$"

View File

@ -17,6 +17,13 @@ class AzureStages(Enum):
TASK_ORDER_BILLING_CREATION = "task order billing creation" TASK_ORDER_BILLING_CREATION = "task order billing creation"
TASK_ORDER_BILLING_VERIFICATION = "task order billing verification" TASK_ORDER_BILLING_VERIFICATION = "task order billing verification"
BILLING_INSTRUCTION = "billing instruction" 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): def _build_csp_states(csp_stages):

View File

@ -168,14 +168,6 @@ class PortfolioStateMachine(
self.portfolio.csp_data.update(response.dict()) self.portfolio.csp_data.update(response.dict())
db.session.add(self.portfolio) db.session.add(self.portfolio)
db.session.commit() 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: except PydanticValidationError as exc:
app.logger.error( app.logger.error(
f"Failed to cast response to valid result class {self.__repr__()}:", f"Failed to cast response to valid result class {self.__repr__()}:",

View File

@ -16,7 +16,7 @@ Ex.
``` ```
{ {
'postgres_root_user': 'EzTEzSNLKQPHuJyPdPloIDCAlcibbl', 'postgres_root_user': 'EzTEzSNLKQPHuJyPdPloIDCAlcibbl',
'postgres_root_password': "2+[A@E4:C=ubb/#R#'n<p|wCW-|%q^" 'postgres_root_password': "2+[A@E4:C=ubb/#R#'n<p|wCW-|%q^" <!-- pragma: allowlist secret -->
} }
``` ```

View File

@ -1,15 +1,18 @@
import pytest
import json import json
from uuid import uuid4
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from uuid import uuid4
import pytest
from tests.factories import ApplicationFactory, EnvironmentFactory from tests.factories import ApplicationFactory, EnvironmentFactory
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
from atst.domain.csp.cloud import AzureCloudProvider from atst.domain.csp.cloud import AzureCloudProvider
from atst.domain.csp.cloud.models import ( from atst.domain.csp.cloud.models import (
AdminRoleDefinitionCSPPayload,
AdminRoleDefinitionCSPResult,
ApplicationCSPPayload, ApplicationCSPPayload,
ApplicationCSPResult, ApplicationCSPResult,
BaseCSPPayload,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
BillingInstructionCSPResult, BillingInstructionCSPResult,
BillingProfileCreationCSPPayload, BillingProfileCreationCSPPayload,
@ -22,15 +25,20 @@ from atst.domain.csp.cloud.models import (
TaskOrderBillingCreationCSPResult, TaskOrderBillingCreationCSPResult,
TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPPayload,
TaskOrderBillingVerificationCSPResult, TaskOrderBillingVerificationCSPResult,
TenantAdminOwnershipCSPPayload,
TenantAdminOwnershipCSPResult,
TenantCSPPayload, TenantCSPPayload,
TenantCSPResult, TenantCSPResult,
TenantPrincipalAppCSPPayload,
TenantPrincipalAppCSPResult,
TenantPrincipalCredentialCSPPayload,
TenantPrincipalCredentialCSPResult,
TenantPrincipalCSPPayload,
TenantPrincipalCSPResult,
TenantPrincipalOwnershipCSPPayload,
TenantPrincipalOwnershipCSPResult,
) )
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" BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31"
@ -94,8 +102,10 @@ MOCK_CREDS = {
} }
def mock_get_secret(azure, func): def mock_get_secret(azure, val=None):
azure.get_secret = func if val is None:
val = json.dumps(MOCK_CREDS)
azure.get_secret = lambda *a, **k: val
return azure return azure
@ -103,12 +113,12 @@ def mock_get_secret(azure, func):
def test_create_application_succeeds(mock_azure: AzureCloudProvider): def test_create_application_succeeds(mock_azure: AzureCloudProvider):
application = ApplicationFactory.create() application = ApplicationFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"}) mock_management_group_create(mock_azure, {"id": "Test Id"})
mock_azure = mock_get_secret(mock_azure)
mock_azure = mock_get_secret(mock_azure, lambda *a, **k: json.dumps(MOCK_CREDS))
payload = ApplicationCSPPayload( payload = ApplicationCSPPayload(
tenant_id="1234", display_name=application.name, parent_id=str(uuid4()) tenant_id="1234", display_name=application.name, parent_id=str(uuid4())
) )
result = mock_azure.create_application(payload) result = mock_azure.create_application(payload)
assert result.id == "Test Id" assert result.id == "Test Id"
@ -154,10 +164,6 @@ def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
def test_create_tenant(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 = Mock()
mock_result.json.return_value = { mock_result.json.return_value = {
"objectId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d", "objectId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
@ -168,7 +174,6 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
mock_azure.sdk.requests.post.return_value = mock_result mock_azure.sdk.requests.post.return_value = mock_result
payload = TenantCSPPayload( payload = TenantCSPPayload(
**dict( **dict(
creds=creds,
user_id="admin", user_id="admin",
password="JediJan13$coot", # pragma: allowlist secret password="JediJan13$coot", # pragma: allowlist secret
domain_name="jediccpospawnedtenant2", domain_name="jediccpospawnedtenant2",
@ -178,6 +183,7 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
password_recovery_email_address="thomas@promptworks.com", password_recovery_email_address="thomas@promptworks.com",
) )
) )
mock_azure = mock_get_secret(mock_azure)
result = mock_azure.create_tenant(payload) result = mock_azure.create_tenant(payload)
body: TenantCSPResult = result.get("body") body: TenantCSPResult = result.get("body")
assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435" assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
@ -205,7 +211,6 @@ def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
country="US", country="US",
postal_code="19109", postal_code="19109",
), ),
creds=creds,
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
billing_profile_display_name="Test Billing Profile", billing_profile_display_name="Test Billing Profile",
billing_account_name=BILLING_ACCOUNT_NAME, billing_account_name=BILLING_ACCOUNT_NAME,
@ -256,7 +261,7 @@ def test_validate_billing_profile_creation(mock_azure: AzureCloudProvider):
payload = BillingProfileVerificationCSPPayload( payload = BillingProfileVerificationCSPPayload(
**dict( **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", 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",
) )
) )
@ -295,7 +300,6 @@ def test_create_billing_profile_tenant_access(mock_azure: AzureCloudProvider):
payload = BillingProfileTenantAccessCSPPayload( payload = BillingProfileTenantAccessCSPPayload(
**dict( **dict(
creds=creds,
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
user_object_id="0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d", user_object_id="0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31", billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31",
@ -327,7 +331,7 @@ def test_create_task_order_billing_creation(mock_azure: AzureCloudProvider):
payload = TaskOrderBillingCreationCSPPayload( payload = TaskOrderBillingCreationCSPPayload(
**dict( **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_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31",
billing_profile_name="KQWI-W2SU-BG7-TGB", billing_profile_name="KQWI-W2SU-BG7-TGB",
) )
@ -387,7 +391,7 @@ def test_create_task_order_billing_verification(mock_azure):
payload = TaskOrderBillingVerificationCSPPayload( payload = TaskOrderBillingVerificationCSPPayload(
**dict( **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", 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",
) )
) )
@ -422,7 +426,7 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider):
payload = BillingInstructionCSPPayload( payload = BillingInstructionCSPPayload(
**dict( **dict(
creds=creds, tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
initial_clin_amount=1000.00, initial_clin_amount=1000.00,
initial_clin_start_date="2020/1/1", initial_clin_start_date="2020/1/1",
initial_clin_end_date="2020/3/1", initial_clin_end_date="2020/3/1",
@ -435,3 +439,173 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider):
result = mock_azure.create_billing_instruction(payload) result = mock_azure.create_billing_instruction(payload)
body: BillingInstructionCSPResult = result.get("body") body: BillingInstructionCSPResult = result.get("body")
assert body.reported_clin_name == "TO1:CLIN001" assert body.reported_clin_name == "TO1:CLIN001"
def test_create_tenant_principal_app(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 = {"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
)
assert result.principal_app_id == "appId"
def test_create_tenant_principal(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": "principal_id"}
mock_azure.sdk.requests.post.return_value = mock_result
mock_azure = mock_get_secret(mock_azure)
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
mock_azure = mock_get_secret(mock_azure)
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_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,
) 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
mock_azure = mock_get_secret(mock_azure)
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(
**{
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
"user_object_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d",
}
)
result: TenantAdminOwnershipCSPResult = mock_azure.create_tenant_admin_ownership(
payload
)
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"

View File

@ -104,10 +104,15 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
FSMStates.TASK_ORDER_BILLING_CREATION_CREATED, FSMStates.TASK_ORDER_BILLING_CREATION_CREATED,
FSMStates.TASK_ORDER_BILLING_VERIFICATION_CREATED, FSMStates.TASK_ORDER_BILLING_VERIFICATION_CREATED,
FSMStates.BILLING_INSTRUCTION_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,
] ]
# 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: if portfolio.csp_data is not None:
csp_data = portfolio.csp_data csp_data = portfolio.csp_data
else: else:
@ -150,7 +155,7 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
collected_data = dict( collected_data = dict(
list(csp_data.items()) + list(portfolio_data.items()) + list(config.items()) 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 assert sm.state == expected_state
if portfolio.csp_data is not None: if portfolio.csp_data is not None:
csp_data = portfolio.csp_data csp_data = portfolio.csp_data

View File

@ -9,6 +9,9 @@ AZURE_CONFIG = {
"AZURE_TENANT_ID": "MOCK", "AZURE_TENANT_ID": "MOCK",
"AZURE_POLICY_LOCATION": "policies", "AZURE_POLICY_LOCATION": "policies",
"AZURE_VAULT_URL": "http://vault", "AZURE_VAULT_URL": "http://vault",
"POWERSHELL_CLIENT_ID": "MOCK",
"AZURE_OWNER_ROLE_DEF_ID": "MOCK",
"AZURE_GRAPH_RESOURCE": "MOCK",
} }
AUTH_CREDENTIALS = { AUTH_CREDENTIALS = {
@ -48,6 +51,12 @@ def mock_credentials():
return Mock(spec=credentials) return Mock(spec=credentials)
def mock_identity():
import azure.identity as identity
return Mock(spec=identity)
def mock_policy(): def mock_policy():
from azure.mgmt.resource import policy from azure.mgmt.resource import policy
@ -72,15 +81,14 @@ def mock_secrets():
return Mock(spec=secrets) return Mock(spec=secrets)
def mock_identity(): def mock_cloud_details():
import azure.identity as identity from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
return Mock(spec=identity) return AZURE_PUBLIC_CLOUD
class MockAzureSDK(object): class MockAzureSDK(object):
def __init__(self): def __init__(self):
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
self.subscription = mock_subscription() self.subscription = mock_subscription()
self.authorization = mock_authorization() self.authorization = mock_authorization()
@ -89,11 +97,11 @@ class MockAzureSDK(object):
self.managementgroups = mock_managementgroups() self.managementgroups = mock_managementgroups()
self.graphrbac = mock_graphrbac() self.graphrbac = mock_graphrbac()
self.credentials = mock_credentials() self.credentials = mock_credentials()
self.identity = mock_identity()
self.policy = mock_policy() self.policy = mock_policy()
self.secrets = mock_secrets() self.secrets = mock_secrets()
self.requests = mock_requests() self.requests = mock_requests()
# may change to a JEDI cloud self.cloud = mock_cloud_details()
self.cloud = AZURE_PUBLIC_CLOUD
self.identity = mock_identity() self.identity = mock_identity()