atst/atat/domain/csp/cloud/azure_cloud_provider.py

1915 lines
75 KiB
Python

import json
import random
import string
from secrets import token_hex, token_urlsafe
from uuid import uuid4
from flask import current_app as app
from atat.utils import sha256_hex
from .cloud_provider_interface import CloudProviderInterface
from .exceptions import (
AuthenticationException,
UserProvisioningException,
ConnectionException,
UnknownServerException,
SecretException,
DomainNameException,
)
from .models import (
AdminRoleDefinitionCSPPayload,
AdminRoleDefinitionCSPResult,
ApplicationCSPPayload,
ApplicationCSPResult,
BillingInstructionCSPPayload,
BillingInstructionCSPResult,
BillingOwnerCSPPayload,
BillingOwnerCSPResult,
BillingProfileCreationCSPPayload,
BillingProfileCreationCSPResult,
BillingProfileTenantAccessCSPPayload,
BillingProfileTenantAccessCSPResult,
BillingProfileVerificationCSPPayload,
BillingProfileVerificationCSPResult,
CostManagementQueryCSPResult,
InitialMgmtGroupCSPPayload,
InitialMgmtGroupCSPResult,
InitialMgmtGroupVerificationCSPPayload,
InitialMgmtGroupVerificationCSPResult,
EnvironmentCSPPayload,
EnvironmentCSPResult,
KeyVaultCredentials,
PrincipalAdminRoleCSPPayload,
PrincipalAdminRoleCSPResult,
ProductPurchaseCSPPayload,
ProductPurchaseCSPResult,
ProductPurchaseVerificationCSPPayload,
ProductPurchaseVerificationCSPResult,
ReportingCSPPayload,
SubscriptionCreationCSPPayload,
SubscriptionCreationCSPResult,
SubscriptionVerificationCSPPayload,
SuscriptionVerificationCSPResult,
TaskOrderBillingCreationCSPPayload,
TaskOrderBillingCreationCSPResult,
TaskOrderBillingVerificationCSPPayload,
TaskOrderBillingVerificationCSPResult,
TenantAdminOwnershipCSPPayload,
TenantAdminOwnershipCSPResult,
TenantAdminCredentialResetCSPPayload,
TenantCSPPayload,
TenantCSPResult,
TenantPrincipalAppCSPPayload,
TenantPrincipalAppCSPResult,
TenantPrincipalCredentialCSPPayload,
TenantPrincipalCredentialCSPResult,
TenantPrincipalCSPPayload,
TenantPrincipalCSPResult,
TenantPrincipalOwnershipCSPPayload,
TenantPrincipalOwnershipCSPResult,
UserCSPPayload,
UserCSPResult,
UserRoleCSPPayload,
UserRoleCSPResult,
)
from .policy import AzurePolicyManager
# 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"
class AzureSDKProvider(object):
def __init__(self):
from azure.mgmt import subscription, authorization, managementgroups
from azure.mgmt.resource import policy
import azure.graphrbac as graphrbac
import azure.common.credentials as credentials
import azure.identity as identity
from azure.keyvault import secrets
from azure.core import exceptions
from msrestazure.azure_cloud import (
AZURE_PUBLIC_CLOUD,
) # TODO: choose cloud type from config
import adal
import requests
self.subscription = subscription
self.policy = policy
self.managementgroups = managementgroups
self.authorization = authorization
self.adal = adal
self.graphrbac = graphrbac
self.credentials = credentials
self.identity = identity
self.azure_exceptions = exceptions
self.secrets = secrets
self.requests = requests
self.cloud = AZURE_PUBLIC_CLOUD
class AzureCloudProvider(CloudProviderInterface):
def __init__(self, config, azure_sdk_provider=None):
self.config = config
self.client_id = config["AZURE_CLIENT_ID"]
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["AZURE_POWERSHELL_CLIENT_ID"]
self.graph_resource = config["AZURE_GRAPH_RESOURCE"]
self.default_aadp_qty = config["AZURE_AADP_QTY"]
self.roles = {
"owner": config["AZURE_ROLE_DEF_ID_OWNER"],
"contributor": config["AZURE_ROLE_DEF_ID_CONTRIBUTOR"],
"billing": config["AZURE_ROLE_DEF_ID_BILLING_READER"],
}
if azure_sdk_provider is None:
self.sdk = AzureSDKProvider()
else:
self.sdk = azure_sdk_provider
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
def set_secret(self, secret_key, secret_value):
credential = self._get_client_secret_credential_obj()
secret_client = self.sdk.secrets.SecretClient(
vault_url=self.vault_url, credential=credential,
)
try:
return secret_client.set_secret(secret_key, secret_value)
except self.sdk.azure_exceptions.HttpResponseError:
app.logger.error(
f"Could not SET secret in Azure keyvault for key {secret_key}.",
exc_info=1,
)
creds = self._source_creds()
raise SecretException(
creds.tenant_id,
f"Could not SET secret in Azure keyvault for key {secret_key}.",
)
def get_secret(self, secret_key):
credential = self._get_client_secret_credential_obj()
secret_client = self.sdk.secrets.SecretClient(
vault_url=self.vault_url, credential=credential,
)
try:
return secret_client.get_secret(secret_key).value
except self.sdk.azure_exceptions.HttpResponseError:
app.logger.error(
f"Could not GET secret in Azure keyvault for key {secret_key}.",
exc_info=1,
)
creds = self._source_creds()
raise SecretException(
creds.tenant_id,
f"Could not GET secret in Azure keyvault for key {secret_key}.",
)
def create_environment(self, payload: EnvironmentCSPPayload):
creds = self._source_creds(payload.tenant_id)
credentials = self._get_credential_obj(
{
"client_id": creds.tenant_sp_client_id,
"secret_key": creds.tenant_sp_key,
"tenant_id": creds.tenant_id,
},
resource=self.sdk.cloud.endpoints.resource_manager,
)
response = self._create_management_group(
credentials,
payload.management_group_name,
payload.display_name,
payload.parent_id,
)
return EnvironmentCSPResult(**response)
def create_application(self, payload: ApplicationCSPPayload):
creds = self._source_creds(payload.tenant_id)
credentials = self._get_credential_obj(
{
"client_id": creds.tenant_sp_client_id,
"secret_key": creds.tenant_sp_key,
"tenant_id": creds.tenant_id,
},
resource=self.sdk.cloud.endpoints.resource_manager,
)
response = self._create_management_group(
credentials,
payload.management_group_name,
payload.display_name,
payload.parent_id,
)
return ApplicationCSPResult(**response)
def create_initial_mgmt_group(self, payload: InitialMgmtGroupCSPPayload):
creds = self._source_creds(payload.tenant_id)
credentials = self._get_credential_obj(
{
"client_id": creds.root_sp_client_id,
"secret_key": creds.root_sp_key,
"tenant_id": creds.root_tenant_id,
},
resource=self.sdk.cloud.endpoints.resource_manager,
)
response = self._create_management_group(
credentials, payload.management_group_name, payload.display_name,
)
return InitialMgmtGroupCSPResult(**response)
def create_initial_mgmt_group_verification(
self, payload: InitialMgmtGroupVerificationCSPPayload
):
creds = self._source_creds(payload.tenant_id)
credentials = self._get_credential_obj(
{
"client_id": creds.root_sp_client_id,
"secret_key": creds.root_sp_key,
"tenant_id": creds.root_tenant_id,
},
resource=self.sdk.cloud.endpoints.resource_manager,
)
response = self._get_management_group(credentials, payload.tenant_id,)
return InitialMgmtGroupVerificationCSPResult(**response.result())
def _create_management_group(
self, credentials, management_group_id, display_name, parent_id=None,
):
mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials)
create_parent_grp_info = self.sdk.managementgroups.models.CreateParentGroupInfo(
id=parent_id
)
create_mgmt_grp_details = self.sdk.managementgroups.models.CreateManagementGroupDetails(
parent=create_parent_grp_info
)
mgmt_grp_create = self.sdk.managementgroups.models.CreateManagementGroupRequest(
name=management_group_id,
display_name=display_name,
details=create_mgmt_grp_details,
)
create_request = mgmgt_group_client.management_groups.create_or_update(
management_group_id, mgmt_grp_create
)
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally
# TODO: what to do is status is not 'Succeeded' on the
# response object? Will it always raise its own error
# instead?
return create_request.result()
def _get_management_group(self, credentials, management_group_id):
mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials)
response = mgmgt_group_client.management_groups.get(management_group_id)
return response
def _create_policy_definition(
self, credentials, subscription_id, management_group_id, properties,
):
"""
Requires credentials that have AZURE_MANAGEMENT_API
specified as the resource. The Service Principal
specified in the credentials must have the "Resource
Policy Contributor" role assigned with a scope at least
as high as the management group specified by
management_group_id.
Arguments:
credentials -- ServicePrincipalCredentials
subscription_id -- str, ID of the subscription (just the UUID, not the path)
management_group_id -- str, ID of the management group (just the UUID, not the path)
properties -- dictionary, the "properties" section of a valid Azure policy definition document
Returns:
azure.mgmt.resource.policy.[api version].models.PolicyDefinition: the PolicyDefinition object provided to Azure
Raises:
TBD
"""
# TODO: which subscription would this be?
client = self.sdk.policy.PolicyClient(credentials, subscription_id)
definition = client.policy_definitions.models.PolicyDefinition(
policy_type=properties.get("policyType"),
mode=properties.get("mode"),
display_name=properties.get("displayName"),
description=properties.get("description"),
policy_rule=properties.get("policyRule"),
parameters=properties.get("parameters"),
)
name = properties.get("displayName")
return client.policy_definitions.create_or_update_at_management_group(
policy_definition_name=name,
parameters=definition,
management_group_id=management_group_id,
)
def disable_user(self, tenant_id, role_assignment_cloud_id):
sp_token = self._get_tenant_principal_token(tenant_id)
if sp_token is None:
raise AuthenticationException("Could not resolve token in disable user")
headers = {
"Authorization": f"Bearer {sp_token}",
}
try:
result = self.sdk.requests.delete(
f"{self.sdk.cloud.endpoints.resource_manager}/{role_assignment_cloud_id}?api-version=2015-07-01",
headers=headers,
timeout=30,
)
result.raise_for_status()
return result.json()
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not disable user. Connection Error", exc_info=1,
)
raise ConnectionException("connection error azure disable user")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not disable user. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error azure disable user")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code, "azure application error disable user", exc_info=1,
)
raise UnknownServerException(
result.status_code, f"azure application error disable user. {str(exc)}",
)
def validate_domain_name(self, name):
response = self.sdk.requests.get(
f"{self.sdk.cloud.endpoints.active_directory}/{name}.onmicrosoft.com/.well-known/openid-configuration",
timeout=30,
)
# If an existing tenant with name cannot be found, 'error' will be in the response
return "error" in response.json()
def generate_valid_domain_name(self, name, suffix="", max_tries=6):
if max_tries > 0:
domain_name = name + suffix
if self.validate_domain_name(domain_name):
return domain_name
else:
suffix = token_hex(3)
return self.generate_valid_domain_name(name, suffix, max_tries - 1)
else:
raise DomainNameException(name)
def create_tenant(self, payload: TenantCSPPayload):
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)
payload.domain_name = self.generate_valid_domain_name(payload.domain_name)
create_tenant_body = payload.dict(by_alias=True)
create_tenant_headers = {
"Authorization": f"Bearer {sp_token}",
}
try:
result = self.sdk.requests.post(
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,
timeout=30,
)
result.raise_for_status()
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create tenant. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating tenant")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create tenant. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error creating tenant")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating tenant",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating tenant. {str(exc)}",
)
result_dict = result.json()
tenant_id = result_dict.get("tenantId")
tenant_admin_username = f"{payload.user_id}@{payload.domain_name}.{self.config.get('OFFICE_365_DOMAIN')}"
self.update_tenant_creds(
tenant_id,
KeyVaultCredentials(
tenant_id=tenant_id,
tenant_admin_username=tenant_admin_username,
tenant_admin_password=payload.password,
),
)
return TenantCSPResult(domain_name=payload.domain_name, **result_dict)
def create_billing_profile_creation(
self, payload: BillingProfileCreationCSPPayload
):
sp_token = self._get_root_provisioning_token()
if sp_token is None:
raise AuthenticationException(
"Could not resolve token for billing profile creation"
)
create_billing_account_body = payload.dict(by_alias=True)
create_billing_account_headers = {
"Authorization": f"Bearer {sp_token}",
}
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"
try:
result = self.sdk.requests.post(
billing_account_create_url,
json=create_billing_account_body,
headers=create_billing_account_headers,
timeout=30,
)
result.raise_for_status()
if result.status_code == 202:
# 202 has location/retry after headers
return BillingProfileCreationCSPResult(**result.headers)
elif result.status_code == 200:
# NB: Swagger docs imply call can sometimes resolve immediately
return BillingProfileVerificationCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create billing profile. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating billing profile")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create billing profile. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error creating tenant")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating billing profile",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating billing profile. {str(exc)}",
)
def create_billing_profile_verification(
self, payload: BillingProfileVerificationCSPPayload
):
sp_token = self._get_root_provisioning_token()
if sp_token is None:
raise AuthenticationException(
"Could not resolve token for billing profile validation"
)
auth_header = {
"Authorization": f"Bearer {sp_token}",
}
try:
result = self.sdk.requests.get(
payload.billing_profile_verify_url, headers=auth_header, timeout=30,
)
result.raise_for_status()
if result.status_code == 202:
# 202 has location/retry after headers
return BillingProfileCreationCSPResult(**result.headers)
elif result.status_code == 200:
return BillingProfileVerificationCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not verify billing profile creation. Connection Error",
exc_info=1,
)
raise ConnectionException(
"connection error creating billing profile verification"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not verify billing profile creation. Request timed out.",
exc_info=1,
)
raise ConnectionException(
"timout error during billing profile verification"
)
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error during billing profile verification",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error during billing profile verification. {str(exc)}",
)
def create_billing_profile_tenant_access(
self, payload: BillingProfileTenantAccessCSPPayload
):
sp_token = self._get_root_provisioning_token()
request_body = {
"properties": {
"principalTenantId": payload.tenant_id, # from tenant creation
"principalId": payload.user_object_id, # from tenant creationn
"roleDefinitionId": f"/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000000",
}
}
headers = {
"Authorization": f"Bearer {sp_token}",
}
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"
try:
result = self.sdk.requests.post(
url, headers=headers, json=request_body, timeout=30,
)
result.raise_for_status()
if result.status_code == 201:
return BillingProfileTenantAccessCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create billing profile tenant access. Connection Error",
exc_info=1,
)
raise ConnectionException(
"connection error creating billing profile tenant access"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create billing profile tenant access. Request timed out.",
exc_info=1,
)
raise ConnectionException(
"timout error creating billing profile tenant access"
)
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating billing profile tenant access",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating billing profile tenant access. {str(exc)}",
)
def create_task_order_billing_creation(
self, payload: TaskOrderBillingCreationCSPPayload
):
sp_token = self._get_root_provisioning_token()
request_body = [
{
"op": "replace",
"path": "/enabledAzurePlans",
"value": [{"skuId": AZURE_SKU_ID}],
}
]
request_headers = {
"Authorization": f"Bearer {sp_token}",
}
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"
try:
result = self.sdk.requests.patch(
url, headers=request_headers, json=request_body, timeout=30,
)
result.raise_for_status()
if result.status_code == 202:
# 202 has location/retry after headers
return TaskOrderBillingCreationCSPResult(**result.headers)
elif result.status_code == 200:
return TaskOrderBillingVerificationCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create task order billing. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating task order billing")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create task order billing. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error creating task order billing")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating task order billing",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating task order billing. {str(exc)}",
)
def create_task_order_billing_verification(
self, payload: TaskOrderBillingVerificationCSPPayload
):
sp_token = self._get_root_provisioning_token()
if sp_token is None:
raise AuthenticationException(
"Could not resolve token for task order billing validation"
)
auth_header = {
"Authorization": f"Bearer {sp_token}",
}
try:
result = self.sdk.requests.get(
payload.task_order_billing_verify_url, headers=auth_header, timeout=30,
)
result.raise_for_status()
if result.status_code == 202:
# 202 has location/retry after headers
return TaskOrderBillingCreationCSPResult(**result.headers)
elif result.status_code == 200:
return TaskOrderBillingVerificationCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not verify task order billing. Connection Error", exc_info=1,
)
raise ConnectionException(
"connection error during task order billing verification"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create verify task order billing. Request timed out.",
exc_info=1,
)
raise ConnectionException("timout error in task order billing verification")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error in task order billing verification",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error in task order billing verification. {str(exc)}",
)
def create_billing_instruction(self, payload: BillingInstructionCSPPayload):
sp_token = self._get_root_provisioning_token()
if sp_token is None:
raise AuthenticationException(
"Could not resolve token for task order billing validation"
)
request_body = {
"properties": {
"amount": payload.initial_clin_amount,
"startDate": payload.initial_clin_start_date,
"endDate": payload.initial_clin_end_date,
}
}
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}",
}
try:
result = self.sdk.requests.put(
url, headers=auth_header, json=request_body, timeout=30
)
result.raise_for_status()
return BillingInstructionCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create billing instructions. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating billing instructions")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create billing instructions. Request timed out.",
exc_info=1,
)
raise ConnectionException("timout error creating billing instructions")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error in creating billing instructions",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error in creating billing instructions. {str(exc)}",
)
def create_subscription(self, payload: SubscriptionCreationCSPPayload):
sp_token = self._get_tenant_principal_token(payload.tenant_id)
if sp_token is None:
raise AuthenticationException(
"Could not resolve token for subscription creation"
)
request_body = {
"displayName": payload.display_name,
"skuId": AZURE_SKU_ID,
"managementGroupId": payload.parent_group_id,
}
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/invoiceSections/{payload.invoice_section_name}/providers/Microsoft.Subscription/createSubscription?api-version=2019-10-01-preview"
auth_header = {
"Authorization": f"Bearer {sp_token}",
}
try:
result = self.sdk.requests.put(
url, headers=auth_header, json=request_body, timeout=30
)
result.raise_for_status()
if result.status_code in [200, 202]:
# 202 has location/retry after headers
return SubscriptionCreationCSPResult(**result.headers, **result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create subscription. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating subscription")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create subscription. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error creating subscription")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating subscription",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating subscription. {str(exc)}",
)
def create_subscription_creation(self, payload: SubscriptionCreationCSPPayload):
return self.create_subscription(payload)
def create_subscription_verification(
self, payload: SubscriptionVerificationCSPPayload
):
sp_token = self._get_tenant_principal_token(payload.tenant_id)
if sp_token is None:
raise AuthenticationException(
"Could not resolve token for subscription verification"
)
auth_header = {
"Authorization": f"Bearer {sp_token}",
}
try:
result = self.sdk.requests.get(
payload.subscription_verify_url, headers=auth_header, timeout=30
)
result.raise_for_status()
# 202 has location/retry after headers
return SuscriptionVerificationCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not verify subscription. Connection Error", exc_info=1,
)
raise ConnectionException(
"connection error during subscription verification"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not verify subscription. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error during subscription verification")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error during subscription verification",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error during subscription verification. {str(exc)}",
)
def create_product_purchase(self, payload: ProductPurchaseCSPPayload):
sp_token = self._get_root_provisioning_token()
if sp_token is None:
raise AuthenticationException(
"Could not resolve token for aad premium product purchase"
)
create_product_purchase_body = {
"type": "AADPremium",
"sku": "AADP1",
"productProperties": {"beneficiaryTenantId": payload.tenant_id,},
"quantity": self.default_aadp_qty,
}
create_product_purchase_headers = {
"Authorization": f"Bearer {sp_token}",
}
product_purchase_url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/purchaseProduct?api-version=2019-10-01-preview"
try:
result = self.sdk.requests.post(
product_purchase_url,
json=create_product_purchase_body,
headers=create_product_purchase_headers,
timeout=30,
)
result.raise_for_status()
if result.status_code == 202:
# 202 has location/retry after headers
return ProductPurchaseCSPResult(**result.headers)
elif result.status_code == 200:
# NB: Swagger docs imply call can sometimes resolve immediately
return ProductPurchaseVerificationCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not purchase product. Connection Error", exc_info=1,
)
raise ConnectionException("connection error during product purchase")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not purchase product. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error during product purchase")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error during product purchase",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error during product purchase. {str(exc)}",
)
def create_product_purchase_verification(
self, payload: ProductPurchaseVerificationCSPPayload
):
sp_token = self._get_root_provisioning_token()
if sp_token is None:
raise AuthenticationException(
"Could not resolve token for aad premium product purchase validation"
)
auth_header = {
"Authorization": f"Bearer {sp_token}",
}
try:
result = self.sdk.requests.get(
payload.product_purchase_verify_url, headers=auth_header, timeout=30
)
result.raise_for_status()
if result.status_code == 202:
# 202 has location/retry after headers
return ProductPurchaseCSPResult(**result.headers)
elif result.status_code == 200:
premium_purchase_date = result.json()["properties"]["purchaseDate"]
return ProductPurchaseVerificationCSPResult(
premium_purchase_date=premium_purchase_date
)
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not verify product purchase. Connection Error", exc_info=1,
)
raise ConnectionException(
"connection error during product purchase verification"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not verify product purchase. Request timed out.", exc_info=1,
)
raise ConnectionException(
"timout error during product purchase verification"
)
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error during product purchase verification",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error during product purchase verification. {str(exc)}",
)
def create_tenant_admin_credential_reset(
self, payload: TenantAdminCredentialResetCSPPayload
):
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"
)
self._update_active_directory_user_password_profile(graph_token, payload)
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.roles['owner']}"
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"
try:
result = self.sdk.requests.put(
url, headers=auth_header, json=request_body, timeout=30
)
result.raise_for_status()
return TenantAdminOwnershipCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create tenant admin ownership. Connection Error",
exc_info=1,
)
raise ConnectionException(
"connection error creating tenant admin ownership"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create tenant admin ownership. Request timed out.",
exc_info=1,
)
raise ConnectionException("timout error creating tenant admin ownership")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating tenant admin ownership",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating tenant admin ownership. {str(exc)}",
)
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.roles['owner']}"
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"
try:
result = self.sdk.requests.put(
url, headers=auth_header, json=request_body, timeout=30,
)
result.raise_for_status()
return TenantPrincipalOwnershipCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create tenant principal ownership. Connection Error",
exc_info=1,
)
raise ConnectionException(
"connection error creating tenant principal ownership"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create tenant principal ownership. Request timed out.",
exc_info=1,
)
raise ConnectionException(
"timout error creating tenant prinicpal ownership"
)
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating tenant principal ownership",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating tenant principal ownership. {str(exc)}",
)
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"
try:
result = self.sdk.requests.post(
url, json=request_body, headers=auth_header, timeout=30
)
result.raise_for_status()
return TenantPrincipalAppCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create tenant principal app. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating tenant principal app")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create tenant principal app. Request timed out.",
exc_info=1,
)
raise ConnectionException("timout error creating tenant principal app")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating tenant principal app",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating tenant principal app. {str(exc)}",
)
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"
try:
result = self.sdk.requests.post(
url, json=request_body, headers=auth_header, timeout=30
)
result.raise_for_status()
return TenantPrincipalCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create tenant principal. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating tenant principal")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create tenant principal. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error creating tenant principal")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating tenant principal",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating tenant principal. {str(exc)}",
)
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"
try:
result = self.sdk.requests.post(
url, json=request_body, headers=auth_header, timeout=30
)
result.raise_for_status()
result_json = result.json()
self.update_tenant_creds(
payload.tenant_id,
KeyVaultCredentials(
tenant_id=payload.tenant_id,
tenant_sp_key=result_json.get("secretText"),
tenant_sp_client_id=payload.principal_app_id,
),
)
return TenantPrincipalCredentialCSPResult(
principal_client_id=payload.principal_app_id,
principal_creds_established=True,
)
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create tenant principal credential. Connection Error",
exc_info=1,
)
raise ConnectionException(
"connection error creating tenant principal credential"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create tenant principal credential. Request timed out.",
exc_info=1,
)
raise ConnectionException(
"timout error creating tenant principal credential"
)
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating tenant principal credential",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating tenant principal credential. {str(exc)}",
)
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"
try:
response = self.sdk.requests.get(url, headers=auth_header, timeout=30)
response.raise_for_status()
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)
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create admin role definition. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating admin role definition")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create admin role definition. Request timed out.",
exc_info=1,
)
raise ConnectionException("timout error creating admin role definition")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
response.status_code,
"azure application error creating admin role definition",
exc_info=1,
)
raise UnknownServerException(
response.status_code,
f"azure application error creating admin role definition. {str(exc)}",
)
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"
try:
result = self.sdk.requests.post(
url, headers=auth_header, json=request_body, timeout=30
)
result.raise_for_status()
return PrincipalAdminRoleCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create principal admin role. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating principal admin role")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create principal admin role. Request timed out.",
exc_info=1,
)
raise ConnectionException("timout error creating principal admin role")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating principal admin role",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating principal admin role. {str(exc)}",
)
def create_billing_owner(self, payload: BillingOwnerCSPPayload):
graph_token = self._get_tenant_principal_token(
payload.tenant_id, resource=self.graph_resource
)
if graph_token is None:
raise AuthenticationException(
"Could not resolve graph token for tenant admin"
)
# Step 1: Create an AAD identity for the user
user_result = self._create_active_directory_user(graph_token, payload)
# Step 2: Set the recovery email
self._update_active_directory_user_email(graph_token, user_result.id, payload)
# Step 3: Find the Billing Administrator role ID
billing_admin_role_id = self._get_billing_owner_role(graph_token)
# Step 4: Assign the Billing Administrator role to the new user
self._assign_billing_owner_role(
graph_token, billing_admin_role_id, user_result.id
)
return BillingOwnerCSPResult(billing_owner_id=user_result.id)
def _assign_billing_owner_role(self, graph_token, billing_admin_role_id, user_id):
request_body = {
"roleDefinitionId": billing_admin_role_id,
"principalId": user_id,
"resourceScope": "/",
}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/beta/roleManagement/directory/roleAssignments"
try:
result = self.sdk.requests.post(url, headers=auth_header, json=request_body)
result.raise_for_status()
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not assign billing owner role. Connection Error", exc_info=1,
)
raise ConnectionException("connection error assigning billing owner role")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not assign billing owner role. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error assigning billing owner role")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error assigning billing owner role",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error assigning billing owner role. {str(exc)}",
)
if result.ok:
return True
else:
raise UserProvisioningException("Could not assign billing admin role")
def _get_billing_owner_role(self, graph_token):
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/v1.0/directoryRoles"
try:
result = self.sdk.requests.get(url, headers=auth_header)
result.raise_for_status()
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not get billing owner role. Connection Error", exc_info=1,
)
raise ConnectionException("connection error getting billing owner role")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not get billing owner role. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error getting billing owner role")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error getting billing owner role",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error getting billing owner role. {str(exc)}",
)
if result.ok:
result = result.json()
for role in result["value"]:
if role["displayName"] == "Billing Administrator":
return role["id"]
else:
raise UserProvisioningException(
"Could not find Billing Administrator role ID; role may not be enabled."
)
def _get_management_service_principal(self):
# we really should be using graph.microsoft.com, but i'm getting
# "expired token" errors for that
# graph_resource = "https://graph.microsoft.com"
graph_resource = "https://graph.windows.net"
graph_creds = self._get_credential_obj(
self._root_creds, resource=graph_resource
)
# I needed to set permissions for the graph.windows.net API before I
# could get this to work.
# how do we scope the graph client to the new subscription rather than
# the cloud0 subscription? tenant id seems to be separate from subscription id
graph_client = self.sdk.graphrbac.GraphRbacManagementClient(
graph_creds, self._root_creds.get("tenant_id")
)
# do we need to create a new application to manage each subscripition
# or should we manage access to each subscription from a single service
# principal with multiple role assignments?
app_display_name = "?" # name should reflect the subscription it exists
app_create_param = self.sdk.graphrbac.models.ApplicationCreateParameters(
display_name=app_display_name
)
# we need the appropriate perms here:
# https://docs.microsoft.com/en-us/graph/api/application-post-applications?view=graph-rest-beta&tabs=http
# https://docs.microsoft.com/en-us/graph/permissions-reference#microsoft-graph-permission-names
# set app perms in app registration portal
# https://docs.microsoft.com/en-us/graph/auth-v2-service#2-configure-permissions-for-microsoft-graph
app: self.sdk.graphrbac.models.Application = graph_client.applications.create(
app_create_param
)
# create a new service principle for the new application, which should be scoped
# to the new subscription
app_id = app.app_id
sp_create_params = self.sdk.graphrbac.models.ServicePrincipalCreateParameters(
app_id=app_id, account_enabled=True
)
service_principal = graph_client.service_principals.create(sp_create_params)
return service_principal
def create_user(self, payload: UserCSPPayload) -> UserCSPResult:
"""Create a user in an Azure Active Directory instance.
Unlike most of the methods on this interface, this requires
two API calls: one POST to create the user and one PATCH to
set the alternate email address. The email address cannot
be set on the first API call. The email address is
necessary so that users can do Self-Service Password
Recovery.
Arguments:
payload {UserCSPPayload} -- a payload object with the
data necessary for both calls
Returns:
UserCSPResult -- a result object containing the AAD ID.
"""
graph_token = self._get_tenant_principal_token(
payload.tenant_id, resource=self.graph_resource
)
if graph_token is None:
raise AuthenticationException(
"Could not resolve graph token for tenant admin"
)
result = self._create_active_directory_user(graph_token, payload)
self._update_active_directory_user_email(graph_token, result.id, payload)
return result
def _create_active_directory_user(self, graph_token, payload):
request_body = {
"accountEnabled": True,
"displayName": payload.display_name,
"mailNickname": payload.mail_nickname,
"userPrincipalName": payload.user_principal_name,
"passwordProfile": {
"forceChangePasswordNextSignIn": True,
"password": payload.password,
},
}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}v1.0/users"
try:
result = self.sdk.requests.post(
url, headers=auth_header, json=request_body, timeout=30
)
result.raise_for_status()
return UserCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not create active directory user. Connection Error", exc_info=1,
)
raise ConnectionException("connection error creating active directory user")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not create active directory user. Request timed out.",
exc_info=1,
)
raise ConnectionException("timout error creating active directory user")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error creating active directory user",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error creating active directory user. {str(exc)}",
)
def _update_active_directory_user_email(self, graph_token, user_id, payload):
request_body = {"otherMails": [payload.email]}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}v1.0/users/{user_id}"
try:
result = self.sdk.requests.patch(
url, headers=auth_header, json=request_body, timeout=30
)
result.raise_for_status()
if result.ok:
return True
else:
raise UserProvisioningException(
f"Failed update user email: {response.json()}"
)
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not update active directory user email. Connection Error",
exc_info=1,
)
raise ConnectionException(
"connection error updating active directory user email"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not update active directory user email. Request timed out.",
exc_info=1,
)
raise ConnectionException(
"timout error updating active directory user email"
)
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error updating active directory user email",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error updating active directory user email. {str(exc)}",
)
def _update_active_directory_user_password_profile(self, graph_token, payload):
request_body = {
"passwordProfile": {
"forceChangePasswordNextSignIn": True,
"forceChangePasswordNextSignInWithMfa": False,
"password": payload.new_password
or "".join(random.choice(string.ascii_letters) for i in range(15)),
}
}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}v1.0/users/{payload.user_id}"
try:
result = self.sdk.requests.patch(
url, headers=auth_header, json=request_body, timeout=30
)
result.raise_for_status()
if result.ok:
return True
else:
raise UserProvisioningException(
f"Failed update user password profile: {response.json()}"
)
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not update active directory user password profile. Connection Error",
exc_info=1,
)
raise ConnectionException(
"connection error updating active directory user password profile"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not update active directory user password profile. Request timed out.",
exc_info=1,
)
raise ConnectionException(
"timout error updating active directory user password profile"
)
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error updating active directory user password profile",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error updating active directory user password profile. {str(exc)}",
)
def create_user_role(self, payload: UserRoleCSPPayload):
graph_token = self._get_tenant_principal_token(payload.tenant_id)
if graph_token is None:
raise AuthenticationException(
"Could not resolve graph token for tenant admin"
)
role_guid = self.roles[payload.role]
role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.management_group_id}/providers/Microsoft.Authorization/roleDefinitions/{role_guid}"
request_body = {
"properties": {
"roleDefinitionId": role_definition_id,
"principalId": payload.user_object_id,
}
}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
assignment_guid = str(uuid4())
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.management_group_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 UserRoleCSPResult(**response.json())
else:
raise UserProvisioningException(
f"Failed to create user role assignment: {response.json()}"
)
def _extract_subscription_id(self, subscription_url):
sub_id_match = SUBSCRIPTION_ID_REGEX.match(subscription_url)
if sub_id_match:
return sub_id_match.group(1)
def _get_tenant_admin_token(self, tenant_id, resource):
creds = self._source_tenant_creds(tenant_id)
return self._get_up_token_for_resource(
creds.tenant_admin_username,
creds.tenant_admin_password,
tenant_id,
resource,
)
def _get_root_provisioning_token(self):
creds = self._source_creds()
return self._get_sp_token(
creds.root_tenant_id, creds.root_sp_client_id, creds.root_sp_key
)
def _get_sp_token(self, tenant_id, client_id, secret_key, resource=None):
context = self.sdk.adal.AuthenticationContext(
f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
)
resource = resource or self.sdk.cloud.endpoints.resource_manager
# TODO: handle failure states here
token_response = context.acquire_token_with_client_credentials(
resource, client_id, secret_key
)
return token_response.get("accessToken", None)
def _get_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"),
secret=creds.get("secret_key"),
tenant=creds.get("tenant_id"),
resource=resource,
cloud_environment=self.sdk.cloud,
)
def _get_client_secret_credential_obj(self):
creds = self._source_creds()
return self.sdk.identity.ClientSecretCredential(
tenant_id=creds.tenant_id,
client_id=creds.root_sp_client_id,
client_secret=creds.root_sp_key,
)
@property
def _root_creds(self):
return {
"client_id": self.client_id,
"secret_key": self.secret_key,
"tenant_id": self.tenant_id,
}
def _get_tenant_principal_token(self, tenant_id, resource=None):
creds = self._source_creds(tenant_id)
return self._get_sp_token(
creds.tenant_id,
creds.tenant_sp_client_id,
creds.tenant_sp_key,
resource=resource,
)
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"
try:
result = self.sdk.requests.post(url, headers=auth_header, timeout=30)
result.raise_for_status()
if not result.ok:
raise AuthenticationException("Failed to elevate access")
return mgmt_token
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not get elevated management token. Connection Error",
exc_info=1,
)
raise ConnectionException(
"connection error getting elevated management token"
)
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not get elevated management token. Request timed out.",
exc_info=1,
)
raise ConnectionException("timout error getting elevated management token")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error getting elevated management token",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error getting elevated management token. {str(exc)}",
)
def _source_creds(self, tenant_id=None) -> KeyVaultCredentials:
if tenant_id:
return self._source_tenant_creds(tenant_id)
else:
return KeyVaultCredentials(
root_tenant_id=self._root_creds.get("tenant_id"),
root_sp_client_id=self._root_creds.get("client_id"),
root_sp_key=self._root_creds.get("secret_key"),
)
def update_tenant_creds(self, tenant_id, secret: KeyVaultCredentials):
hashed = sha256_hex(tenant_id)
curr_secrets = self._source_tenant_creds(tenant_id)
updated_secrets = curr_secrets.merge_credentials(secret)
self.set_secret(hashed, json.dumps(updated_secrets.dict()))
return updated_secrets
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))
def get_reporting_data(self, payload: ReportingCSPPayload):
"""
Queries the Cost Management API for an invoice section's raw reporting data
We query at the invoiceSection scope. The full scope path is passed in
with the payload at the `invoice_section_id` key.
"""
creds = self._source_tenant_creds(payload.tenant_id)
token = self._get_sp_token(
payload.tenant_id, creds.tenant_sp_client_id, creds.tenant_sp_key
)
if not token:
raise AuthenticationException("Could not retrieve tenant access token")
headers = {"Authorization": f"Bearer {token}"}
request_body = {
"type": "Usage",
"timeframe": "Custom",
"timePeriod": {"from": payload.from_date, "to": payload.to_date,},
"dataset": {
"granularity": "Monthly",
"aggregation": {"totalCost": {"name": "PreTaxCost", "function": "Sum"}},
"grouping": [{"type": "Dimension", "name": "InvoiceId"}],
},
}
cost_mgmt_url = (
f"/providers/Microsoft.CostManagement/query?api-version=2019-11-01"
)
try:
result = self.sdk.requests.post(
f"{self.sdk.cloud.endpoints.resource_manager}{payload.invoice_section_id}{cost_mgmt_url}",
json=request_body,
headers=headers,
timeout=30,
)
result.raise_for_status()
if result.ok:
return CostManagementQueryCSPResult(**result.json())
except self.sdk.requests.exceptions.ConnectionError:
app.logger.error(
f"Could not get reporting data. Connection Error", exc_info=1,
)
raise ConnectionException("connection error getting reporting data")
except self.sdk.requests.exceptions.Timeout:
app.logger.error(
f"Could not get reporting data. Request timed out.", exc_info=1,
)
raise ConnectionException("timout error getting reporting data")
except self.sdk.requests.exceptions.HTTPError as exc:
app.logger.error(
result.status_code,
"azure application error getting reporting data",
exc_info=1,
)
raise UnknownServerException(
result.status_code,
f"azure application error getting reporting data. {str(exc)}",
)
def _get_calculator_creds(self):
authority = f"{self.sdk.cloud.endpoints.active_directory}/{self.tenant_id}"
context = self.sdk.adal.AuthenticationContext(authority=authority)
response = context.acquire_token_with_client_credentials(
self.config.get("AZURE_CALC_RESOURCE"),
self.config.get("AZURE_CALC_CLIENT_ID"),
self.config.get("AZURE_CALC_SECRET"),
)
return response.get("accessToken")
def get_calculator_url(self):
calc_access_token = self._get_calculator_creds()
return f"{self.config.get('AZURE_CALC_URL')}?access_token={calc_access_token}"