Update atst to atat
This commit is contained in:
31
atat/domain/csp/__init__.py
Normal file
31
atat/domain/csp/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from .cloud import MockCloudProvider
|
||||
from .files import AzureFileService, MockFileService
|
||||
from .reports import MockReportingProvider
|
||||
|
||||
|
||||
class MockCSP:
|
||||
def __init__(self, app, test_mode=False):
|
||||
self.cloud = MockCloudProvider(
|
||||
app.config,
|
||||
with_delay=(not test_mode),
|
||||
with_failure=(not test_mode),
|
||||
with_authorization=(not test_mode),
|
||||
)
|
||||
self.files = MockFileService(app)
|
||||
self.reports = MockReportingProvider()
|
||||
|
||||
|
||||
class AzureCSP:
|
||||
def __init__(self, app):
|
||||
self.cloud = MockCloudProvider(app.config)
|
||||
self.files = AzureFileService(app.config)
|
||||
self.reports = MockReportingProvider()
|
||||
|
||||
|
||||
def make_csp_provider(app, csp=None):
|
||||
if csp == "azure":
|
||||
app.csp = AzureCSP(app)
|
||||
elif csp == "mock-test":
|
||||
app.csp = MockCSP(app, test_mode=True)
|
||||
else:
|
||||
app.csp = MockCSP(app)
|
3
atat/domain/csp/cloud/__init__.py
Normal file
3
atat/domain/csp/cloud/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .azure_cloud_provider import AzureCloudProvider
|
||||
from .cloud_provider_interface import CloudProviderInterface
|
||||
from .mock_cloud_provider import MockCloudProvider
|
1841
atat/domain/csp/cloud/azure_cloud_provider.py
Normal file
1841
atat/domain/csp/cloud/azure_cloud_provider.py
Normal file
File diff suppressed because it is too large
Load Diff
87
atat/domain/csp/cloud/cloud_provider_interface.py
Normal file
87
atat/domain/csp/cloud/cloud_provider_interface.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class CloudProviderInterface: # pragma: no cover
|
||||
def set_secret(self, secret_key: str, secret_value: str):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_secret(self, secret_key: str):
|
||||
raise NotImplementedError()
|
||||
|
||||
def root_creds(self) -> Dict:
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_environment(self, payload):
|
||||
"""Create a new environment in the CSP.
|
||||
|
||||
Arguments:
|
||||
auth_credentials -- Object containing CSP account credentials
|
||||
user -- ATAT user authorizing the environment creation
|
||||
environment -- ATAT Environment model
|
||||
|
||||
Returns:
|
||||
string: ID of created environment
|
||||
|
||||
Raises:
|
||||
AuthenticationException: Problem with the credentials
|
||||
AuthorizationException: Credentials not authorized for current action(s)
|
||||
ConnectionException: Issue with the CSP API connection
|
||||
UnknownServerException: Unknown issue on the CSP side
|
||||
EnvironmentExistsException: Environment already exists and has been created
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_or_update_user(
|
||||
self, auth_credentials: Dict, user_info, csp_role_id: str
|
||||
) -> str:
|
||||
"""Creates a user or updates an existing user's role.
|
||||
|
||||
Arguments:
|
||||
auth_credentials -- Object containing CSP account credentials
|
||||
user_info -- instance of EnvironmentRole containing user data
|
||||
if it has a csp_user_id it will try to update that user
|
||||
csp_role_id -- The id of the role the user should be given in the CSP
|
||||
|
||||
Returns:
|
||||
string: Returns the interal csp_user_id of the created/updated user account
|
||||
|
||||
Raises:
|
||||
AuthenticationException: Problem with the credentials
|
||||
AuthorizationException: Credentials not authorized for current action(s)
|
||||
ConnectionException: Issue with the CSP API connection
|
||||
UnknownServerException: Unknown issue on the CSP side
|
||||
UserProvisioningException: User couldn't be created or modified
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def disable_user(self, tenant_id: str, role_assignment_cloud_id: str) -> bool:
|
||||
"""Revoke all privileges for a user. Used to prevent user access while a full
|
||||
delete is being processed.
|
||||
|
||||
Arguments:
|
||||
tenant_id -- CSP internal tenant identifier
|
||||
role_assignment_cloud_id -- CSP name of the role assignment to delete.
|
||||
|
||||
Returns:
|
||||
bool -- True on success
|
||||
|
||||
Raises:
|
||||
AuthenticationException: Problem with the credentials
|
||||
AuthorizationException: Credentials not authorized for current action(s)
|
||||
ConnectionException: Issue with the CSP API connection
|
||||
UnknownServerException: Unknown issue on the CSP side
|
||||
UserRemovalException: User couldn't be suspended
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_calculator_url(self) -> str:
|
||||
"""Returns the calculator url for the CSP.
|
||||
This will likely be a static property elsewhere once a CSP is chosen.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_environment_login_url(self, environment) -> str:
|
||||
"""Returns the login url for a given environment
|
||||
This may move to be a computed property on the Environment domain object
|
||||
"""
|
||||
raise NotImplementedError()
|
146
atat/domain/csp/cloud/exceptions.py
Normal file
146
atat/domain/csp/cloud/exceptions.py
Normal file
@@ -0,0 +1,146 @@
|
||||
class GeneralCSPException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OperationInProgressException(GeneralCSPException):
|
||||
"""Throw this for instances when the CSP reports that the current entity is already
|
||||
being operated on/created/deleted/etc
|
||||
"""
|
||||
|
||||
def __init__(self, operation_desc):
|
||||
self.operation_desc = operation_desc
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "An operation for this entity is already in progress: {}".format(
|
||||
self.operation_desc
|
||||
)
|
||||
|
||||
|
||||
class AuthenticationException(GeneralCSPException):
|
||||
"""Throw this for instances when there is a problem with the auth credentials:
|
||||
* Missing credentials
|
||||
* Incorrect credentials
|
||||
* Other credential problems
|
||||
"""
|
||||
|
||||
def __init__(self, auth_error):
|
||||
self.auth_error = auth_error
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "An error occurred with authentication: {}".format(self.auth_error)
|
||||
|
||||
|
||||
class AuthorizationException(GeneralCSPException):
|
||||
"""Throw this for instances when the current credentials are not authorized
|
||||
for the current action.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_error):
|
||||
self.auth_error = auth_error
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "An error occurred with authorization: {}".format(self.auth_error)
|
||||
|
||||
|
||||
class ConnectionException(GeneralCSPException):
|
||||
"""A general problem with the connection, timeouts or unresolved endpoints
|
||||
"""
|
||||
|
||||
def __init__(self, connection_error):
|
||||
self.connection_error = connection_error
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "Could not connect to cloud provider: {}".format(self.connection_error)
|
||||
|
||||
|
||||
class UnknownServerException(GeneralCSPException):
|
||||
"""An error occured on the CSP side (5xx) and we don't know why
|
||||
"""
|
||||
|
||||
def __init__(self, status_code, server_error):
|
||||
self.status_code = status_code
|
||||
self.server_error = server_error
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return f"A server error with status code [{self.status_code}] occured: {self.server_error}"
|
||||
|
||||
|
||||
class EnvironmentCreationException(GeneralCSPException):
|
||||
"""If there was an error in creating the environment
|
||||
"""
|
||||
|
||||
def __init__(self, env_identifier, reason):
|
||||
self.env_identifier = env_identifier
|
||||
self.reason = reason
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "The envionment {} couldn't be created: {}".format(
|
||||
self.env_identifier, self.reason
|
||||
)
|
||||
|
||||
|
||||
class UserProvisioningException(GeneralCSPException):
|
||||
"""Failed to provision a user
|
||||
"""
|
||||
|
||||
|
||||
class UserRemovalException(GeneralCSPException):
|
||||
"""Failed to remove a user
|
||||
"""
|
||||
|
||||
def __init__(self, user_csp_id, reason):
|
||||
self.user_csp_id = user_csp_id
|
||||
self.reason = reason
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "Failed to suspend or delete user {}: {}".format(
|
||||
self.user_csp_id, self.reason
|
||||
)
|
||||
|
||||
|
||||
class BaselineProvisionException(GeneralCSPException):
|
||||
"""If there's any issues standing up whatever is required
|
||||
for an environment baseline
|
||||
"""
|
||||
|
||||
def __init__(self, env_identifier, reason):
|
||||
self.env_identifier = env_identifier
|
||||
self.reason = reason
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "Could not complete baseline provisioning for environment ({}): {}".format(
|
||||
self.env_identifier, self.reason
|
||||
)
|
||||
|
||||
|
||||
class SecretException(GeneralCSPException):
|
||||
"""A problem occurred with setting or getting secrets"""
|
||||
|
||||
def __init__(self, tenant_id, reason):
|
||||
self.tenant_id = tenant_id
|
||||
self.reason = reason
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "Could not get or set secret for ({}): {}".format(
|
||||
self.tenant_id, self.reason
|
||||
)
|
||||
|
||||
|
||||
class DomainNameException(GeneralCSPException):
|
||||
"""A problem occured when generating the domain name for a tenant"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return f"Could not generate unique tenant name for {self.name}"
|
519
atat/domain/csp/cloud/mock_cloud_provider.py
Normal file
519
atat/domain/csp/cloud/mock_cloud_provider.py
Normal file
@@ -0,0 +1,519 @@
|
||||
from uuid import uuid4
|
||||
import pendulum
|
||||
|
||||
from .cloud_provider_interface import CloudProviderInterface
|
||||
from .exceptions import (
|
||||
AuthenticationException,
|
||||
AuthorizationException,
|
||||
ConnectionException,
|
||||
GeneralCSPException,
|
||||
UnknownServerException,
|
||||
UserProvisioningException,
|
||||
UserRemovalException,
|
||||
)
|
||||
from .models import (
|
||||
AZURE_MGMNT_PATH,
|
||||
AdminRoleDefinitionCSPPayload,
|
||||
AdminRoleDefinitionCSPResult,
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
BillingInstructionCSPResult,
|
||||
BillingOwnerCSPPayload,
|
||||
BillingOwnerCSPResult,
|
||||
BillingProfileCreationCSPPayload,
|
||||
BillingProfileCreationCSPResult,
|
||||
BillingProfileTenantAccessCSPResult,
|
||||
BillingProfileVerificationCSPPayload,
|
||||
BillingProfileVerificationCSPResult,
|
||||
InitialMgmtGroupCSPPayload,
|
||||
InitialMgmtGroupCSPResult,
|
||||
InitialMgmtGroupVerificationCSPPayload,
|
||||
InitialMgmtGroupVerificationCSPResult,
|
||||
CostManagementQueryCSPResult,
|
||||
CostManagementQueryProperties,
|
||||
ProductPurchaseCSPPayload,
|
||||
ProductPurchaseCSPResult,
|
||||
ProductPurchaseVerificationCSPPayload,
|
||||
ProductPurchaseVerificationCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
ReportingCSPPayload,
|
||||
SubscriptionCreationCSPPayload,
|
||||
SubscriptionCreationCSPResult,
|
||||
SubscriptionVerificationCSPPayload,
|
||||
SuscriptionVerificationCSPResult,
|
||||
EnvironmentCSPPayload,
|
||||
EnvironmentCSPResult,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
TaskOrderBillingVerificationCSPResult,
|
||||
TenantAdminOwnershipCSPPayload,
|
||||
TenantAdminOwnershipCSPResult,
|
||||
TenantCSPPayload,
|
||||
TenantCSPResult,
|
||||
TenantPrincipalAppCSPPayload,
|
||||
TenantPrincipalAppCSPResult,
|
||||
TenantPrincipalCredentialCSPPayload,
|
||||
TenantPrincipalCredentialCSPResult,
|
||||
TenantPrincipalCSPPayload,
|
||||
TenantPrincipalCSPResult,
|
||||
TenantPrincipalOwnershipCSPPayload,
|
||||
TenantPrincipalOwnershipCSPResult,
|
||||
UserCSPPayload,
|
||||
UserCSPResult,
|
||||
)
|
||||
|
||||
|
||||
class MockCloudProvider(CloudProviderInterface):
|
||||
|
||||
# TODO: All of these constants
|
||||
AUTHENTICATION_EXCEPTION = AuthenticationException("Authentication failure.")
|
||||
AUTHORIZATION_EXCEPTION = AuthorizationException("Not authorized.")
|
||||
NETWORK_EXCEPTION = ConnectionException("Network failure.")
|
||||
SERVER_EXCEPTION = UnknownServerException(500, "Not our fault.")
|
||||
|
||||
SERVER_FAILURE_PCT = 1
|
||||
NETWORK_FAILURE_PCT = 7
|
||||
ENV_CREATE_FAILURE_PCT = 12
|
||||
ATAT_ADMIN_CREATE_FAILURE_PCT = 12
|
||||
UNAUTHORIZED_RATE = 2
|
||||
|
||||
def __init__(
|
||||
self, config, with_delay=True, with_failure=True, with_authorization=True
|
||||
):
|
||||
from time import sleep
|
||||
import random
|
||||
|
||||
self._with_delay = with_delay
|
||||
self._with_failure = with_failure
|
||||
self._with_authorization = with_authorization
|
||||
self._sleep = sleep
|
||||
self._random = random
|
||||
|
||||
def root_creds(self):
|
||||
return self._auth_credentials
|
||||
|
||||
def set_secret(self, secret_key: str, secret_value: str):
|
||||
pass
|
||||
|
||||
def get_secret(self, secret_key: str, default=dict()):
|
||||
return default
|
||||
|
||||
def create_subscription(self, payload: SubscriptionCreationCSPPayload):
|
||||
return self.create_subscription_creation(payload)
|
||||
|
||||
def create_subscription_creation(self, payload: SubscriptionCreationCSPPayload):
|
||||
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 SubscriptionCreationCSPResult(
|
||||
subscription_verify_url="https://zombo.com", subscription_retry_after=10
|
||||
)
|
||||
|
||||
def create_subscription_verification(
|
||||
self, payload: SubscriptionVerificationCSPPayload
|
||||
):
|
||||
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 SuscriptionVerificationCSPResult(
|
||||
subscription_id="subscriptions/60fbbb72-0516-4253-ab18-c92432ba3230"
|
||||
)
|
||||
|
||||
def create_tenant(self, payload: TenantCSPPayload):
|
||||
"""
|
||||
payload is an instance of TenantCSPPayload data class
|
||||
"""
|
||||
|
||||
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 TenantCSPResult(
|
||||
**{
|
||||
"tenant_id": "",
|
||||
"user_id": "",
|
||||
"user_object_id": "",
|
||||
"domain_name": "",
|
||||
"tenant_admin_username": "test",
|
||||
"tenant_admin_password": "test",
|
||||
}
|
||||
)
|
||||
|
||||
def create_billing_profile_creation(
|
||||
self, payload: BillingProfileCreationCSPPayload
|
||||
):
|
||||
# response will be mostly the same as the body, but we only really care about the id
|
||||
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 BillingProfileCreationCSPResult(
|
||||
**dict(
|
||||
billing_profile_verify_url="https://zombo.com",
|
||||
billing_profile_retry_after=10,
|
||||
)
|
||||
)
|
||||
|
||||
def create_billing_profile_verification(
|
||||
self, payload: BillingProfileVerificationCSPPayload
|
||||
):
|
||||
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 BillingProfileVerificationCSPResult(
|
||||
**{
|
||||
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB",
|
||||
"name": "KQWI-W2SU-BG7-TGB",
|
||||
"properties": {
|
||||
"address": {
|
||||
"addressLine1": "123 S Broad Street, Suite 2400",
|
||||
"city": "Philadelphia",
|
||||
"companyName": "Promptworks",
|
||||
"country": "US",
|
||||
"postalCode": "19109",
|
||||
"region": "PA",
|
||||
},
|
||||
"currency": "USD",
|
||||
"displayName": "Test Billing Profile",
|
||||
"enabledAzurePlans": [],
|
||||
"hasReadAccess": True,
|
||||
"invoiceDay": 5,
|
||||
"invoiceEmailOptIn": False,
|
||||
"invoiceSections": [
|
||||
{
|
||||
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/invoiceSections/CHCO-BAAR-PJA-TGB",
|
||||
"name": "CHCO-BAAR-PJA-TGB",
|
||||
"properties": {"displayName": "Test Billing Profile"},
|
||||
"type": "Microsoft.Billing/billingAccounts/billingProfiles/invoiceSections",
|
||||
}
|
||||
],
|
||||
},
|
||||
"type": "Microsoft.Billing/billingAccounts/billingProfiles",
|
||||
}
|
||||
)
|
||||
|
||||
def create_billing_profile_tenant_access(self, payload):
|
||||
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 BillingProfileTenantAccessCSPResult(
|
||||
**{
|
||||
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/billingRoleAssignments/40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
|
||||
"name": "40000000-aaaa-bbbb-cccc-100000000000_0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
|
||||
"properties": {
|
||||
"createdOn": "2020-01-14T14:39:26.3342192+00:00",
|
||||
"createdByPrincipalId": "82e2b376-3297-4096-8743-ed65b3be0b03",
|
||||
"principalId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
|
||||
"principalTenantId": "60ff9d34-82bf-4f21-b565-308ef0533435",
|
||||
"roleDefinitionId": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000000",
|
||||
"scope": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/KQWI-W2SU-BG7-TGB",
|
||||
},
|
||||
"type": "Microsoft.Billing/billingRoleAssignments",
|
||||
}
|
||||
)
|
||||
|
||||
def create_task_order_billing_creation(
|
||||
self, payload: TaskOrderBillingCreationCSPPayload
|
||||
):
|
||||
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 TaskOrderBillingCreationCSPResult(
|
||||
**{"Location": "https://somelocation", "Retry-After": "10"}
|
||||
)
|
||||
|
||||
def create_task_order_billing_verification(
|
||||
self, payload: TaskOrderBillingVerificationCSPPayload
|
||||
):
|
||||
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 TaskOrderBillingVerificationCSPResult(
|
||||
**{
|
||||
"id": "/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/billingProfiles/XC36-GRNZ-BG7-TGB",
|
||||
"name": "XC36-GRNZ-BG7-TGB",
|
||||
"properties": {
|
||||
"address": {
|
||||
"addressLine1": "123 S Broad Street, Suite 2400",
|
||||
"city": "Philadelphia",
|
||||
"companyName": "Promptworks",
|
||||
"country": "US",
|
||||
"postalCode": "19109",
|
||||
"region": "PA",
|
||||
},
|
||||
"currency": "USD",
|
||||
"displayName": "First Portfolio Billing Profile",
|
||||
"enabledAzurePlans": [
|
||||
{
|
||||
"productId": "DZH318Z0BPS6",
|
||||
"skuId": "0001",
|
||||
"skuDescription": "Microsoft Azure Plan",
|
||||
}
|
||||
],
|
||||
"hasReadAccess": True,
|
||||
"invoiceDay": 5,
|
||||
"invoiceEmailOptIn": False,
|
||||
},
|
||||
"type": "Microsoft.Billing/billingAccounts/billingProfiles",
|
||||
}
|
||||
)
|
||||
|
||||
def create_billing_instruction(self, payload: BillingInstructionCSPPayload):
|
||||
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 BillingInstructionCSPResult(
|
||||
**{
|
||||
"name": "TO1:CLIN001",
|
||||
"properties": {
|
||||
"amount": 1000.0,
|
||||
"endDate": "2020-03-01T00:00:00+00:00",
|
||||
"startDate": "2020-01-01T00:00:00+00:00",
|
||||
},
|
||||
"type": "Microsoft.Billing/billingAccounts/billingProfiles/billingInstructions",
|
||||
}
|
||||
)
|
||||
|
||||
def create_initial_mgmt_group(self, payload: InitialMgmtGroupCSPPayload):
|
||||
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 InitialMgmtGroupCSPResult(
|
||||
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}",
|
||||
)
|
||||
|
||||
def create_initial_mgmt_group_verification(
|
||||
self, payload: InitialMgmtGroupVerificationCSPPayload
|
||||
):
|
||||
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 InitialMgmtGroupVerificationCSPResult(
|
||||
**dict(
|
||||
id="Test Id"
|
||||
# id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
|
||||
)
|
||||
)
|
||||
|
||||
def create_product_purchase(self, payload: ProductPurchaseCSPPayload):
|
||||
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 ProductPurchaseCSPResult(
|
||||
**dict(
|
||||
product_purchase_verify_url="https://zombo.com",
|
||||
product_purchase_retry_after=10,
|
||||
)
|
||||
)
|
||||
|
||||
def create_product_purchase_verification(
|
||||
self, payload: ProductPurchaseVerificationCSPPayload
|
||||
):
|
||||
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 ProductPurchaseVerificationCSPResult(
|
||||
**dict(premium_purchase_date="2020-01-30T18:57:05.981Z")
|
||||
)
|
||||
|
||||
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_billing_owner(self, payload: BillingOwnerCSPPayload):
|
||||
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 BillingOwnerCSPResult(billing_owner_id="foo")
|
||||
|
||||
def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
|
||||
self._authorize(auth_credentials)
|
||||
|
||||
self._delay(1, 5)
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(
|
||||
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
||||
UserProvisioningException(
|
||||
user_info.environment.id,
|
||||
user_info.application_role.user_id,
|
||||
"Could not create user.",
|
||||
),
|
||||
)
|
||||
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
return self._id()
|
||||
|
||||
def disable_user(self, tenant_id, role_assignment_cloud_id):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
|
||||
self._maybe_raise(
|
||||
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
||||
UserRemovalException(tenant_id, "Could not disable user."),
|
||||
)
|
||||
|
||||
return self._maybe(12)
|
||||
|
||||
def get_calculator_url(self):
|
||||
return "https://www.rackspace.com/en-us/calculator"
|
||||
|
||||
def get_environment_login_url(self, environment):
|
||||
"""Returns the login url for a given environment
|
||||
"""
|
||||
return "https://www.mycloud.com/my-env-login"
|
||||
|
||||
def _id(self):
|
||||
return uuid4().hex
|
||||
|
||||
def _delay(self, min_secs, max_secs):
|
||||
if self._with_delay:
|
||||
duration = self._random.randrange(min_secs, max_secs)
|
||||
self._sleep(duration)
|
||||
|
||||
def _maybe(self, pct):
|
||||
return not self._with_failure or self._random.randrange(0, 100) < pct
|
||||
|
||||
def _maybe_raise(self, pct, exc):
|
||||
if self._with_failure and self._maybe(pct):
|
||||
raise exc
|
||||
|
||||
@property
|
||||
def _auth_credentials(self):
|
||||
return {"username": "mock-cloud", "password": "shh"} # pragma: allowlist secret
|
||||
|
||||
def _authorize(self, credentials):
|
||||
self._delay(1, 5)
|
||||
if self._with_authorization and credentials != self._auth_credentials:
|
||||
raise self.AUTHENTICATION_EXCEPTION
|
||||
|
||||
def create_application(self, payload: ApplicationCSPPayload):
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||
|
||||
return ApplicationCSPResult(
|
||||
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
|
||||
)
|
||||
|
||||
def create_environment(self, payload: EnvironmentCSPPayload):
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||
|
||||
return EnvironmentCSPResult(
|
||||
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
|
||||
)
|
||||
|
||||
def create_user(self, payload: UserCSPPayload):
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||
|
||||
return UserCSPResult(id=str(uuid4()))
|
||||
|
||||
def get_credentials(self, scope="portfolio", tenant_id=None):
|
||||
return self.root_creds()
|
||||
|
||||
def update_tenant_creds(self, tenant_id, secret):
|
||||
return secret
|
||||
|
||||
def get_reporting_data(self, payload: ReportingCSPPayload):
|
||||
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)
|
||||
object_id = str(uuid4())
|
||||
|
||||
start_of_month = pendulum.today(tz="utc").start_of("month").replace(tzinfo=None)
|
||||
this_month = start_of_month.to_atom_string()
|
||||
last_month = start_of_month.subtract(months=1).to_atom_string()
|
||||
two_months_ago = start_of_month.subtract(months=2).to_atom_string()
|
||||
|
||||
properties = CostManagementQueryProperties(
|
||||
**dict(
|
||||
columns=[
|
||||
{"name": "PreTaxCost", "type": "Number"},
|
||||
{"name": "BillingMonth", "type": "Datetime"},
|
||||
{"name": "InvoiceId", "type": "String"},
|
||||
{"name": "Currency", "type": "String"},
|
||||
],
|
||||
rows=[
|
||||
[1.0, two_months_ago, "", "USD"],
|
||||
[500.0, two_months_ago, "e05009w9sf", "USD"],
|
||||
[50.0, last_month, "", "USD"],
|
||||
[1000.0, last_month, "e0500a4qhw", "USD"],
|
||||
[500.0, this_month, "", "USD"],
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
return CostManagementQueryCSPResult(
|
||||
**dict(name=object_id, properties=properties,)
|
||||
)
|
621
atat/domain/csp/cloud/models.py
Normal file
621
atat/domain/csp/cloud/models.py
Normal file
@@ -0,0 +1,621 @@
|
||||
from enum import Enum
|
||||
from secrets import token_urlsafe
|
||||
from typing import Dict, List, Optional
|
||||
from uuid import uuid4
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel, validator, root_validator
|
||||
|
||||
from .utils import (
|
||||
generate_mail_nickname,
|
||||
generate_user_principal_name,
|
||||
)
|
||||
from atat.utils import snake_to_camel
|
||||
|
||||
|
||||
class AliasModel(BaseModel):
|
||||
"""
|
||||
This provides automatic camel <-> snake conversion for serializing to/from json
|
||||
You can override the alias generation in subclasses by providing a Config that defines
|
||||
a fields property with a dict mapping variables to their cast names, for cases like:
|
||||
* some_url:someURL
|
||||
* user_object_id:objectId
|
||||
"""
|
||||
|
||||
class Config:
|
||||
alias_generator = snake_to_camel
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class BaseCSPPayload(AliasModel):
|
||||
tenant_id: str
|
||||
|
||||
|
||||
class TenantCSPPayload(AliasModel):
|
||||
user_id: str
|
||||
password: Optional[str]
|
||||
domain_name: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
country_code: str
|
||||
password_recovery_email_address: str
|
||||
|
||||
|
||||
class TenantCSPResult(AliasModel):
|
||||
user_id: str
|
||||
tenant_id: str
|
||||
user_object_id: str
|
||||
domain_name: str
|
||||
|
||||
tenant_admin_username: Optional[str]
|
||||
tenant_admin_password: Optional[str]
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"user_object_id": "objectId",
|
||||
}
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
exclude = {"tenant_admin_username", "tenant_admin_password"}
|
||||
if "exclude" not in kwargs:
|
||||
kwargs["exclude"] = exclude
|
||||
else:
|
||||
kwargs["exclude"].update(exclude)
|
||||
|
||||
return super().dict(*args, **kwargs)
|
||||
|
||||
def get_creds(self):
|
||||
return {
|
||||
"tenant_admin_username": self.tenant_admin_username,
|
||||
"tenant_admin_password": self.tenant_admin_password,
|
||||
"tenant_id": self.tenant_id,
|
||||
}
|
||||
|
||||
|
||||
class BillingProfileAddress(AliasModel):
|
||||
company_name: str
|
||||
address_line_1: str
|
||||
city: str
|
||||
region: str
|
||||
country: str
|
||||
postal_code: str
|
||||
|
||||
|
||||
class BillingProfileCLINBudget(AliasModel):
|
||||
clin_budget: Dict
|
||||
"""
|
||||
"clinBudget": {
|
||||
"amount": 0,
|
||||
"startDate": "2019-12-18T16:47:40.909Z",
|
||||
"endDate": "2019-12-18T16:47:40.909Z",
|
||||
"externalReferenceId": "string"
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class BillingProfileCreationCSPPayload(BaseCSPPayload):
|
||||
tenant_id: str
|
||||
billing_profile_display_name: str
|
||||
billing_account_name: str
|
||||
enabled_azure_plans: Optional[List[str]]
|
||||
address: BillingProfileAddress
|
||||
|
||||
@validator("enabled_azure_plans", pre=True, always=True)
|
||||
def default_enabled_azure_plans(cls, v):
|
||||
"""
|
||||
Normally you'd implement this by setting the field with a value of:
|
||||
dataclasses.field(default_factory=list)
|
||||
but that prevents the object from being correctly pickled, so instead we need
|
||||
to rely on a validator to ensure this has an empty value when not specified
|
||||
"""
|
||||
return v or []
|
||||
|
||||
class Config:
|
||||
fields = {"billing_profile_display_name": "displayName"}
|
||||
|
||||
|
||||
class BillingProfileCreationCSPResult(AliasModel):
|
||||
billing_profile_verify_url: str
|
||||
billing_profile_retry_after: int
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"billing_profile_verify_url": "Location",
|
||||
"billing_profile_retry_after": "Retry-After",
|
||||
}
|
||||
|
||||
|
||||
class BillingProfileVerificationCSPPayload(BaseCSPPayload):
|
||||
billing_profile_verify_url: str
|
||||
|
||||
|
||||
class BillingInvoiceSection(AliasModel):
|
||||
invoice_section_id: str
|
||||
invoice_section_name: str
|
||||
|
||||
class Config:
|
||||
fields = {"invoice_section_id": "id", "invoice_section_name": "name"}
|
||||
|
||||
|
||||
class BillingProfileProperties(AliasModel):
|
||||
address: BillingProfileAddress
|
||||
billing_profile_display_name: str
|
||||
invoice_sections: List[BillingInvoiceSection]
|
||||
|
||||
class Config:
|
||||
fields = {"billing_profile_display_name": "displayName"}
|
||||
|
||||
|
||||
class BillingProfileVerificationCSPResult(AliasModel):
|
||||
billing_profile_id: str
|
||||
billing_profile_name: str
|
||||
billing_profile_properties: BillingProfileProperties
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"billing_profile_id": "id",
|
||||
"billing_profile_name": "name",
|
||||
"billing_profile_properties": "properties",
|
||||
}
|
||||
|
||||
|
||||
class BillingProfileTenantAccessCSPPayload(BaseCSPPayload):
|
||||
tenant_id: str
|
||||
user_object_id: str
|
||||
billing_account_name: str
|
||||
billing_profile_name: str
|
||||
|
||||
|
||||
class BillingProfileTenantAccessCSPResult(AliasModel):
|
||||
billing_role_assignment_id: str
|
||||
billing_role_assignment_name: str
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"billing_role_assignment_id": "id",
|
||||
"billing_role_assignment_name": "name",
|
||||
}
|
||||
|
||||
|
||||
class TaskOrderBillingCreationCSPPayload(BaseCSPPayload):
|
||||
billing_account_name: str
|
||||
billing_profile_name: str
|
||||
|
||||
|
||||
class TaskOrderBillingCreationCSPResult(AliasModel):
|
||||
task_order_billing_verify_url: str
|
||||
task_order_retry_after: int
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"task_order_billing_verify_url": "Location",
|
||||
"task_order_retry_after": "Retry-After",
|
||||
}
|
||||
|
||||
|
||||
class TaskOrderBillingVerificationCSPPayload(BaseCSPPayload):
|
||||
task_order_billing_verify_url: str
|
||||
|
||||
|
||||
class BillingProfileEnabledPlanDetails(AliasModel):
|
||||
enabled_azure_plans: List[Dict]
|
||||
|
||||
|
||||
class TaskOrderBillingVerificationCSPResult(AliasModel):
|
||||
billing_profile_id: str
|
||||
billing_profile_name: str
|
||||
billing_profile_enabled_plan_details: BillingProfileEnabledPlanDetails
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"billing_profile_id": "id",
|
||||
"billing_profile_name": "name",
|
||||
"billing_profile_enabled_plan_details": "properties",
|
||||
}
|
||||
|
||||
|
||||
class BillingInstructionCSPPayload(BaseCSPPayload):
|
||||
initial_clin_amount: float
|
||||
initial_clin_start_date: str
|
||||
initial_clin_end_date: str
|
||||
initial_clin_type: str
|
||||
initial_task_order_id: str
|
||||
billing_account_name: str
|
||||
billing_profile_name: str
|
||||
|
||||
|
||||
class BillingInstructionCSPResult(AliasModel):
|
||||
reported_clin_name: str
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"reported_clin_name": "name",
|
||||
}
|
||||
|
||||
|
||||
class TenantAdminOwnershipCSPPayload(BaseCSPPayload):
|
||||
user_object_id: str
|
||||
|
||||
|
||||
class TenantAdminOwnershipCSPResult(AliasModel):
|
||||
admin_owner_assignment_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"admin_owner_assignment_id": "id"}
|
||||
|
||||
|
||||
class TenantPrincipalOwnershipCSPPayload(BaseCSPPayload):
|
||||
principal_id: str
|
||||
|
||||
|
||||
class TenantPrincipalOwnershipCSPResult(AliasModel):
|
||||
principal_owner_assignment_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"principal_owner_assignment_id": "id"}
|
||||
|
||||
|
||||
class TenantPrincipalAppCSPPayload(BaseCSPPayload):
|
||||
pass
|
||||
|
||||
|
||||
class TenantPrincipalAppCSPResult(AliasModel):
|
||||
principal_app_id: str
|
||||
principal_app_object_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"principal_app_id": "appId", "principal_app_object_id": "id"}
|
||||
|
||||
|
||||
class TenantPrincipalCSPPayload(BaseCSPPayload):
|
||||
principal_app_id: str
|
||||
|
||||
|
||||
class TenantPrincipalCSPResult(AliasModel):
|
||||
principal_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"principal_id": "id"}
|
||||
|
||||
|
||||
class TenantPrincipalCredentialCSPPayload(BaseCSPPayload):
|
||||
principal_app_id: str
|
||||
principal_app_object_id: str
|
||||
|
||||
|
||||
class TenantPrincipalCredentialCSPResult(AliasModel):
|
||||
principal_client_id: str
|
||||
principal_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/"
|
||||
|
||||
MANAGEMENT_GROUP_NAME_REGEX = "^[a-zA-Z0-9\-_\(\)\.]+$"
|
||||
|
||||
|
||||
class ManagementGroupCSPPayload(AliasModel):
|
||||
"""
|
||||
:param: management_group_name: Just pass a UUID for this.
|
||||
:param: display_name: This can contain any character and
|
||||
spaces, but should be 90 characters or fewer long.
|
||||
:param: parent_id: This should be the fully qualified Azure ID,
|
||||
i.e. /providers/Microsoft.Management/managementGroups/[management group ID]
|
||||
"""
|
||||
|
||||
tenant_id: str
|
||||
management_group_name: Optional[str]
|
||||
display_name: str
|
||||
parent_id: Optional[str]
|
||||
|
||||
@validator("management_group_name", pre=True, always=True)
|
||||
def supply_management_group_name_default(cls, name):
|
||||
if name:
|
||||
if re.match(MANAGEMENT_GROUP_NAME_REGEX, name) is None:
|
||||
raise ValueError(
|
||||
f"Management group name must match {MANAGEMENT_GROUP_NAME_REGEX}"
|
||||
)
|
||||
|
||||
return name[0:90]
|
||||
else:
|
||||
return str(uuid4())
|
||||
|
||||
@validator("display_name", pre=True, always=True)
|
||||
def enforce_display_name_length(cls, name):
|
||||
return name[0:90]
|
||||
|
||||
@validator("parent_id", pre=True, always=True)
|
||||
def enforce_parent_id_pattern(cls, id_):
|
||||
if id_:
|
||||
if AZURE_MGMNT_PATH not in id_:
|
||||
return f"{AZURE_MGMNT_PATH}{id_}"
|
||||
else:
|
||||
return id_
|
||||
|
||||
|
||||
class ManagementGroupCSPResponse(AliasModel):
|
||||
id: str
|
||||
|
||||
|
||||
class ManagementGroupGetCSPPayload(BaseCSPPayload):
|
||||
management_group_name: str
|
||||
|
||||
|
||||
class ManagementGroupGetCSPResponse(AliasModel):
|
||||
id: str
|
||||
|
||||
|
||||
class ApplicationCSPPayload(ManagementGroupCSPPayload):
|
||||
pass
|
||||
|
||||
|
||||
class ApplicationCSPResult(ManagementGroupCSPResponse):
|
||||
pass
|
||||
|
||||
|
||||
class InitialMgmtGroupCSPPayload(ManagementGroupCSPPayload):
|
||||
pass
|
||||
|
||||
|
||||
class InitialMgmtGroupCSPResult(ManagementGroupCSPResponse):
|
||||
pass
|
||||
|
||||
|
||||
class InitialMgmtGroupVerificationCSPPayload(ManagementGroupGetCSPPayload):
|
||||
pass
|
||||
|
||||
|
||||
class InitialMgmtGroupVerificationCSPResult(ManagementGroupGetCSPResponse):
|
||||
pass
|
||||
|
||||
|
||||
class EnvironmentCSPPayload(ManagementGroupCSPPayload):
|
||||
pass
|
||||
|
||||
|
||||
class EnvironmentCSPResult(ManagementGroupCSPResponse):
|
||||
pass
|
||||
|
||||
|
||||
class KeyVaultCredentials(BaseModel):
|
||||
root_sp_client_id: Optional[str]
|
||||
root_sp_key: Optional[str]
|
||||
root_tenant_id: Optional[str]
|
||||
|
||||
tenant_id: Optional[str]
|
||||
|
||||
tenant_admin_username: Optional[str]
|
||||
tenant_admin_password: Optional[str]
|
||||
|
||||
tenant_sp_client_id: Optional[str]
|
||||
tenant_sp_key: Optional[str]
|
||||
|
||||
@root_validator(pre=True)
|
||||
def enforce_admin_creds(cls, values):
|
||||
tenant_id = values.get("tenant_id")
|
||||
username = values.get("tenant_admin_username")
|
||||
password = values.get("tenant_admin_password")
|
||||
if any([username, password]) and not all([tenant_id, username, password]):
|
||||
raise ValueError(
|
||||
"tenant_id, tenant_admin_username, and tenant_admin_password must all be set if any one is"
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
@root_validator(pre=True)
|
||||
def enforce_sp_creds(cls, values):
|
||||
tenant_id = values.get("tenant_id")
|
||||
client_id = values.get("tenant_sp_client_id")
|
||||
key = values.get("tenant_sp_key")
|
||||
if any([client_id, key]) and not all([tenant_id, client_id, key]):
|
||||
raise ValueError(
|
||||
"tenant_id, tenant_sp_client_id, and tenant_sp_key must all be set if any one is"
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
@root_validator(pre=True)
|
||||
def enforce_root_creds(cls, values):
|
||||
sp_creds = [
|
||||
values.get("root_tenant_id"),
|
||||
values.get("root_sp_client_id"),
|
||||
values.get("root_sp_key"),
|
||||
]
|
||||
if any(sp_creds) and not all(sp_creds):
|
||||
raise ValueError(
|
||||
"root_tenant_id, root_sp_client_id, and root_sp_key must all be set if any one is"
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
def merge_credentials(
|
||||
self, new_creds: "KeyVaultCredentials"
|
||||
) -> "KeyVaultCredentials":
|
||||
updated_creds = {k: v for k, v in new_creds.dict().items() if v}
|
||||
old_creds = self.dict()
|
||||
old_creds.update(updated_creds)
|
||||
|
||||
return KeyVaultCredentials(**old_creds)
|
||||
|
||||
|
||||
class SubscriptionCreationCSPPayload(BaseCSPPayload):
|
||||
display_name: str
|
||||
parent_group_id: str
|
||||
billing_account_name: str
|
||||
billing_profile_name: str
|
||||
invoice_section_name: str
|
||||
|
||||
|
||||
class SubscriptionCreationCSPResult(AliasModel):
|
||||
subscription_verify_url: str
|
||||
subscription_retry_after: int
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"subscription_verify_url": "Location",
|
||||
"subscription_retry_after": "Retry-After",
|
||||
}
|
||||
|
||||
|
||||
class SubscriptionVerificationCSPPayload(BaseCSPPayload):
|
||||
subscription_verify_url: str
|
||||
|
||||
|
||||
SUBSCRIPTION_ID_REGEX = re.compile(
|
||||
"\/?subscriptions\/([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
class SuscriptionVerificationCSPResult(AliasModel):
|
||||
subscription_id: str
|
||||
|
||||
@validator("subscription_id", pre=True, always=True)
|
||||
def enforce_display_name_length(cls, sub_id):
|
||||
sub_id_match = SUBSCRIPTION_ID_REGEX.match(sub_id)
|
||||
if sub_id_match:
|
||||
return sub_id_match.group(1)
|
||||
|
||||
return False
|
||||
|
||||
class Config:
|
||||
fields = {"subscription_id": "subscriptionLink"}
|
||||
|
||||
|
||||
class ProductPurchaseCSPPayload(BaseCSPPayload):
|
||||
billing_account_name: str
|
||||
billing_profile_name: str
|
||||
|
||||
|
||||
class ProductPurchaseCSPResult(AliasModel):
|
||||
product_purchase_verify_url: str
|
||||
product_purchase_retry_after: int
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
"product_purchase_verify_url": "Location",
|
||||
"product_purchase_retry_after": "Retry-After",
|
||||
}
|
||||
|
||||
|
||||
class ProductPurchaseVerificationCSPPayload(BaseCSPPayload):
|
||||
product_purchase_verify_url: str
|
||||
|
||||
|
||||
class ProductPurchaseVerificationCSPResult(AliasModel):
|
||||
premium_purchase_date: str
|
||||
|
||||
|
||||
class UserMixin(BaseModel):
|
||||
password: Optional[str]
|
||||
|
||||
@property
|
||||
def user_principal_name(self):
|
||||
return generate_user_principal_name(self.display_name, self.tenant_host_name)
|
||||
|
||||
@property
|
||||
def mail_nickname(self):
|
||||
return generate_mail_nickname(self.display_name)
|
||||
|
||||
@validator("password", pre=True, always=True)
|
||||
def supply_password_default(cls, password):
|
||||
return password or token_urlsafe(16)
|
||||
|
||||
|
||||
class UserCSPPayload(BaseCSPPayload, UserMixin):
|
||||
display_name: str
|
||||
tenant_host_name: str
|
||||
email: str
|
||||
|
||||
|
||||
class UserCSPResult(AliasModel):
|
||||
id: str
|
||||
|
||||
|
||||
class UserRoleCSPPayload(BaseCSPPayload):
|
||||
class Roles(str, Enum):
|
||||
owner = "owner"
|
||||
contributor = "contributor"
|
||||
billing = "billing"
|
||||
|
||||
management_group_id: str
|
||||
role: Roles
|
||||
user_object_id: str
|
||||
|
||||
|
||||
class UserRoleCSPResult(AliasModel):
|
||||
id: str
|
||||
|
||||
|
||||
class QueryColumn(AliasModel):
|
||||
name: str
|
||||
type: str
|
||||
|
||||
|
||||
class CostManagementQueryProperties(AliasModel):
|
||||
columns: List[QueryColumn]
|
||||
rows: List[Optional[list]]
|
||||
|
||||
|
||||
class CostManagementQueryCSPResult(AliasModel):
|
||||
name: str
|
||||
properties: CostManagementQueryProperties
|
||||
|
||||
|
||||
class ReportingCSPPayload(BaseCSPPayload):
|
||||
invoice_section_id: str
|
||||
from_date: str
|
||||
to_date: str
|
||||
|
||||
@root_validator(pre=True)
|
||||
def extract_invoice_section(cls, values):
|
||||
try:
|
||||
values["invoice_section_id"] = values["billing_profile_properties"][
|
||||
"invoice_sections"
|
||||
][0]["invoice_section_id"]
|
||||
return values
|
||||
except (KeyError, IndexError):
|
||||
raise ValueError("Invoice section ID not present in payload")
|
||||
|
||||
|
||||
class BillingOwnerCSPPayload(BaseCSPPayload, UserMixin):
|
||||
"""
|
||||
This class needs to consume data in the shape it's in from the
|
||||
top-level portfolio CSP data, but return it in the shape
|
||||
needed for user provisioning.
|
||||
"""
|
||||
|
||||
display_name = "billing_admin"
|
||||
domain_name: str
|
||||
password_recovery_email_address: str
|
||||
|
||||
@property
|
||||
def tenant_host_name(self):
|
||||
return self.domain_name
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self.password_recovery_email_address
|
||||
|
||||
|
||||
class BillingOwnerCSPResult(AliasModel):
|
||||
billing_owner_id: str
|
47
atat/domain/csp/cloud/policy.py
Normal file
47
atat/domain/csp/cloud/policy.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from glob import glob
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from os.path import join as path_join
|
||||
|
||||
|
||||
class AzurePolicyManager:
|
||||
def __init__(self, static_policy_location):
|
||||
self._static_policy_location = static_policy_location
|
||||
|
||||
@property
|
||||
def portfolio_definitions(self):
|
||||
if getattr(self, "_portfolio_definitions", None) is None:
|
||||
portfolio_files = self._glob_json("portfolios")
|
||||
self._portfolio_definitions = self._load_policies(portfolio_files)
|
||||
|
||||
return self._portfolio_definitions
|
||||
|
||||
@property
|
||||
def application_definitions(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def environment_definitions(self):
|
||||
pass
|
||||
|
||||
def _glob_json(self, path):
|
||||
return glob(path_join(self._static_policy_location, "portfolios", "*.json"))
|
||||
|
||||
def _load_policies(self, json_policies):
|
||||
return [self._load_policy(pol) for pol in json_policies]
|
||||
|
||||
def _load_policy(self, policy_file):
|
||||
with open(policy_file, "r") as file_:
|
||||
doc = json.loads(file_.read())
|
||||
return AzurePolicy(
|
||||
definition_point=doc["definitionPoint"],
|
||||
definition=doc["policyDefinition"],
|
||||
parameters=doc["parameters"],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AzurePolicy:
|
||||
definition_point: str
|
||||
definition: dict
|
||||
parameters: dict
|
10
atat/domain/csp/cloud/utils.py
Normal file
10
atat/domain/csp/cloud/utils.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from flask import current_app as app
|
||||
|
||||
|
||||
def generate_user_principal_name(name, domain_name):
|
||||
mail_name = generate_mail_nickname(name)
|
||||
return f"{mail_name}@{domain_name}.{app.config.get('OFFICE_365_DOMAIN')}"
|
||||
|
||||
|
||||
def generate_mail_nickname(name):
|
||||
return name.replace(" ", ".").lower()
|
107
atat/domain/csp/files.py
Normal file
107
atat/domain/csp/files.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from uuid import uuid4
|
||||
import pendulum
|
||||
|
||||
|
||||
class FileService:
|
||||
def generate_token(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_download_link(self, object_name, filename) -> (dict, str):
|
||||
raise NotImplementedError()
|
||||
|
||||
def object_name(self) -> str:
|
||||
return str(uuid4())
|
||||
|
||||
def download_task_order(self, object_name):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class MockFileService(FileService):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
def get_token(self):
|
||||
return ({}, self.object_name())
|
||||
|
||||
def generate_download_link(self, object_name, filename):
|
||||
return ""
|
||||
|
||||
def download_task_order(self, object_name):
|
||||
with open("tests/fixtures/sample.pdf", "rb") as some_bytes:
|
||||
return {
|
||||
"name": object_name,
|
||||
"content": some_bytes,
|
||||
}
|
||||
|
||||
|
||||
class AzureFileService(FileService):
|
||||
def __init__(self, config):
|
||||
self.account_name = config["AZURE_ACCOUNT_NAME"]
|
||||
self.storage_key = config["AZURE_STORAGE_KEY"]
|
||||
self.container_name = config["AZURE_TO_BUCKET_NAME"]
|
||||
self.timeout = config["PERMANENT_SESSION_LIFETIME"]
|
||||
|
||||
from azure.storage.common import CloudStorageAccount
|
||||
from azure.storage.blob import BlobSasPermissions
|
||||
from azure.storage.blob.models import BlobPermissions
|
||||
from azure.storage.blob.blockblobservice import BlockBlobService
|
||||
|
||||
self.CloudStorageAccount = CloudStorageAccount
|
||||
self.BlobSasPermissions = BlobSasPermissions
|
||||
self.BlobPermissions = BlobPermissions
|
||||
self.BlockBlobService = BlockBlobService
|
||||
|
||||
def get_token(self):
|
||||
"""
|
||||
Generates an Azure SAS token for pre-authorizing a file upload.
|
||||
|
||||
Returns a tuple in the following format: (token_dict, object_name), where
|
||||
- token_dict has a `token` key which contains the SAS token as a string
|
||||
- object_name is a string
|
||||
"""
|
||||
account = self.CloudStorageAccount(
|
||||
account_name=self.account_name, account_key=self.storage_key
|
||||
)
|
||||
bbs = account.create_block_blob_service()
|
||||
object_name = self.object_name()
|
||||
sas_token = bbs.generate_blob_shared_access_signature(
|
||||
self.container_name,
|
||||
object_name,
|
||||
permission=self.BlobSasPermissions(create=True),
|
||||
expiry=pendulum.now(tz="utc").add(self.timeout),
|
||||
protocol="https",
|
||||
)
|
||||
return ({"token": sas_token}, object_name)
|
||||
|
||||
def generate_download_link(self, object_name, filename):
|
||||
block_blob_service = self.BlockBlobService(
|
||||
account_name=self.account_name, account_key=self.storage_key
|
||||
)
|
||||
sas_token = block_blob_service.generate_blob_shared_access_signature(
|
||||
container_name=self.container_name,
|
||||
blob_name=object_name,
|
||||
permission=self.BlobPermissions(read=True),
|
||||
expiry=pendulum.now(tz="utc").add(self.timeout),
|
||||
content_disposition=f"attachment; filename={filename}",
|
||||
protocol="https",
|
||||
)
|
||||
return block_blob_service.make_blob_url(
|
||||
container_name=self.container_name,
|
||||
blob_name=object_name,
|
||||
protocol="https",
|
||||
sas_token=sas_token,
|
||||
)
|
||||
|
||||
def download_task_order(self, object_name):
|
||||
block_blob_service = self.BlockBlobService(
|
||||
account_name=self.account_name, account_key=self.storage_key
|
||||
)
|
||||
# TODO: We should downloading errors more gracefully
|
||||
# - what happens when we try to request a TO that doesn't exist?
|
||||
b = block_blob_service.get_blob_to_bytes(
|
||||
container_name=self.container_name, blob_name=object_name,
|
||||
)
|
||||
return {
|
||||
"name": b.name,
|
||||
"content": b.content,
|
||||
}
|
35
atat/domain/csp/reports.py
Normal file
35
atat/domain/csp/reports.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
import pendulum
|
||||
|
||||
|
||||
def load_fixture_data():
|
||||
with open("fixtures/fixture_spend_data.json") as json_file:
|
||||
return json.load(json_file)
|
||||
|
||||
|
||||
class MockReportingProvider:
|
||||
FIXTURE_SPEND_DATA = load_fixture_data()
|
||||
|
||||
|
||||
def prepare_azure_reporting_data(rows: list):
|
||||
"""
|
||||
Returns a dict representing invoiced and estimated funds for a portfolio given
|
||||
a list of rows from CostManagementQueryCSPResult.properties.rows
|
||||
{
|
||||
invoiced: Decimal,
|
||||
estimated: Decimal
|
||||
}
|
||||
"""
|
||||
|
||||
estimated = []
|
||||
while rows:
|
||||
if pendulum.parse(rows[-1][1]) >= pendulum.now(tz="utc").start_of("month"):
|
||||
estimated.append(rows.pop())
|
||||
else:
|
||||
break
|
||||
|
||||
return dict(
|
||||
invoiced=Decimal(sum([row[0] for row in rows])),
|
||||
estimated=Decimal(sum([row[0] for row in estimated])),
|
||||
)
|
Reference in New Issue
Block a user