Update atst to atat

This commit is contained in:
leigh-mil
2020-02-28 16:01:45 -05:00
parent 6eb48239cf
commit c2814416fb
215 changed files with 735 additions and 746 deletions

View 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)

View File

@@ -0,0 +1,3 @@
from .azure_cloud_provider import AzureCloudProvider
from .cloud_provider_interface import CloudProviderInterface
from .mock_cloud_provider import MockCloudProvider

File diff suppressed because it is too large Load Diff

View 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()

View 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}"

View 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,)
)

View 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

View 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

View 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
View 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,
}

View 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])),
)