resolve merge conflict with staging

This commit is contained in:
Philip Kalinsky 2020-01-30 15:57:06 -05:00
commit 7e4340e7e4
62 changed files with 1339 additions and 325 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null "lines": null
}, },
"generated_at": "2020-01-19T20:21:20Z", "generated_at": "2020-01-29T16:40:16Z",
"plugins_used": [ "plugins_used": [
{ {
"base64_limit": 4.5, "base64_limit": 4.5,
@ -145,7 +145,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207", "hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 649, "line_number": 647,
"type": "Hex High Entropy String" "type": "Hex High Entropy String"
} }
] ]

View File

@ -257,6 +257,7 @@ To generate coverage reports for the Javascript tests:
- `SESSION_COOKIE_DOMAIN`: String value specifying the name to use for the session cookie. This should be set to the root domain so that it is valid for both the main site and the authentication subdomain. https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_DOMAIN - `SESSION_COOKIE_DOMAIN`: String value specifying the name to use for the session cookie. This should be set to the root domain so that it is valid for both the main site and the authentication subdomain. https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_DOMAIN
- `SESSION_KEY_PREFIX`: A prefix that is added before all session keys: https://pythonhosted.org/Flask-Session/#configuration - `SESSION_KEY_PREFIX`: A prefix that is added before all session keys: https://pythonhosted.org/Flask-Session/#configuration
- `SESSION_TYPE`: String value specifying the cookie storage backend. https://pythonhosted.org/Flask-Session/ - `SESSION_TYPE`: String value specifying the cookie storage backend. https://pythonhosted.org/Flask-Session/
- `SESSION_COOKIE_SECURE`: https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_SECURE
- `SESSION_USE_SIGNER`: Boolean value specifying if the cookie sid should be signed. - `SESSION_USE_SIGNER`: Boolean value specifying if the cookie sid should be signed.
- `SQLALCHEMY_ECHO`: Boolean value specifying if SQLAlchemy should log queries to stdout. - `SQLALCHEMY_ECHO`: Boolean value specifying if SQLAlchemy should log queries to stdout.
- `STATIC_URL`: URL specifying where static assets are hosted. - `STATIC_URL`: URL specifying where static assets are hosted.

View File

@ -0,0 +1,198 @@
"""Admin and Ownership Provisioning Steps
Revision ID: cd7e3f9a5d64
Revises: 508957112ed6
Create Date: 2020-01-30 10:38:04.182953
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "cd7e3f9a5d64" # pragma: allowlist secret
down_revision = "508957112ed6" # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"portfolio_state_machines",
"state",
type_=sa.Enum(
"UNSTARTED",
"STARTING",
"STARTED",
"COMPLETED",
"FAILED",
"TENANT_CREATED",
"TENANT_IN_PROGRESS",
"TENANT_FAILED",
"BILLING_PROFILE_CREATION_CREATED",
"BILLING_PROFILE_CREATION_IN_PROGRESS",
"BILLING_PROFILE_CREATION_FAILED",
"BILLING_PROFILE_VERIFICATION_CREATED",
"BILLING_PROFILE_VERIFICATION_IN_PROGRESS",
"BILLING_PROFILE_VERIFICATION_FAILED",
"BILLING_PROFILE_TENANT_ACCESS_CREATED",
"BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS",
"BILLING_PROFILE_TENANT_ACCESS_FAILED",
"TASK_ORDER_BILLING_CREATION_CREATED",
"TASK_ORDER_BILLING_CREATION_IN_PROGRESS",
"TASK_ORDER_BILLING_CREATION_FAILED",
"TASK_ORDER_BILLING_VERIFICATION_CREATED",
"TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS",
"TASK_ORDER_BILLING_VERIFICATION_FAILED",
"BILLING_INSTRUCTION_CREATED",
"BILLING_INSTRUCTION_IN_PROGRESS",
"BILLING_INSTRUCTION_FAILED",
"TENANT_PRINCIPAL_APP_CREATED",
"TENANT_PRINCIPAL_APP_IN_PROGRESS",
"TENANT_PRINCIPAL_APP_FAILED",
"TENANT_PRINCIPAL_CREATED",
"TENANT_PRINCIPAL_IN_PROGRESS",
"TENANT_PRINCIPAL_FAILED",
"TENANT_PRINCIPAL_CREDENTIAL_CREATED",
"TENANT_PRINCIPAL_CREDENTIAL_IN_PROGRESS",
"TENANT_PRINCIPAL_CREDENTIAL_FAILED",
"ADMIN_ROLE_DEFINITION_CREATED",
"ADMIN_ROLE_DEFINITION_IN_PROGRESS",
"ADMIN_ROLE_DEFINITION_FAILED",
"PRINCIPAL_ADMIN_ROLE_CREATED",
"PRINCIPAL_ADMIN_ROLE_IN_PROGRESS",
"PRINCIPAL_ADMIN_ROLE_FAILED",
"TENANT_ADMIN_OWNERSHIP_CREATED",
"TENANT_ADMIN_OWNERSHIP_IN_PROGRESS",
"TENANT_ADMIN_OWNERSHIP_FAILED",
"TENANT_PRINCIPAL_OWNERSHIP_CREATED",
"TENANT_PRINCIPAL_OWNERSHIP_IN_PROGRESS",
"TENANT_PRINCIPAL_OWNERSHIP_FAILED",
name="fsmstates",
native_enum=False,
),
existing_type=sa.Enum(
"UNSTARTED",
"STARTING",
"STARTED",
"COMPLETED",
"FAILED",
"TENANT_CREATED",
"TENANT_IN_PROGRESS",
"TENANT_FAILED",
"BILLING_PROFILE_CREATION_CREATED",
"BILLING_PROFILE_CREATION_IN_PROGRESS",
"BILLING_PROFILE_CREATION_FAILED",
"BILLING_PROFILE_VERIFICATION_CREATED",
"BILLING_PROFILE_VERIFICATION_IN_PROGRESS",
"BILLING_PROFILE_VERIFICATION_FAILED",
"BILLING_PROFILE_TENANT_ACCESS_CREATED",
"BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS",
"BILLING_PROFILE_TENANT_ACCESS_FAILED",
"TASK_ORDER_BILLING_CREATION_CREATED",
"TASK_ORDER_BILLING_CREATION_IN_PROGRESS",
"TASK_ORDER_BILLING_CREATION_FAILED",
"TASK_ORDER_BILLING_VERIFICATION_CREATED",
"TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS",
"TASK_ORDER_BILLING_VERIFICATION_FAILED",
"BILLING_INSTRUCTION_CREATED",
"BILLING_INSTRUCTION_IN_PROGRESS",
"BILLING_INSTRUCTION_FAILED",
name="fsmstates",
native_enum=False,
create_constraint=False,
),
existing_nullable=False,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"portfolio_state_machines",
"state",
type_=sa.Enum(
"UNSTARTED",
"STARTING",
"STARTED",
"COMPLETED",
"FAILED",
"TENANT_CREATED",
"TENANT_IN_PROGRESS",
"TENANT_FAILED",
"BILLING_PROFILE_CREATION_CREATED",
"BILLING_PROFILE_CREATION_IN_PROGRESS",
"BILLING_PROFILE_CREATION_FAILED",
"BILLING_PROFILE_VERIFICATION_CREATED",
"BILLING_PROFILE_VERIFICATION_IN_PROGRESS",
"BILLING_PROFILE_VERIFICATION_FAILED",
"BILLING_PROFILE_TENANT_ACCESS_CREATED",
"BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS",
"BILLING_PROFILE_TENANT_ACCESS_FAILED",
"TASK_ORDER_BILLING_CREATION_CREATED",
"TASK_ORDER_BILLING_CREATION_IN_PROGRESS",
"TASK_ORDER_BILLING_CREATION_FAILED",
"TASK_ORDER_BILLING_VERIFICATION_CREATED",
"TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS",
"TASK_ORDER_BILLING_VERIFICATION_FAILED",
"BILLING_INSTRUCTION_CREATED",
"BILLING_INSTRUCTION_IN_PROGRESS",
"BILLING_INSTRUCTION_FAILED",
name="fsmstates",
native_enum=False,
),
existing_type=sa.Enum(
"UNSTARTED",
"STARTING",
"STARTED",
"COMPLETED",
"FAILED",
"TENANT_CREATED",
"TENANT_IN_PROGRESS",
"TENANT_FAILED",
"BILLING_PROFILE_CREATION_CREATED",
"BILLING_PROFILE_CREATION_IN_PROGRESS",
"BILLING_PROFILE_CREATION_FAILED",
"BILLING_PROFILE_VERIFICATION_CREATED",
"BILLING_PROFILE_VERIFICATION_IN_PROGRESS",
"BILLING_PROFILE_VERIFICATION_FAILED",
"BILLING_PROFILE_TENANT_ACCESS_CREATED",
"BILLING_PROFILE_TENANT_ACCESS_IN_PROGRESS",
"BILLING_PROFILE_TENANT_ACCESS_FAILED",
"TASK_ORDER_BILLING_CREATION_CREATED",
"TASK_ORDER_BILLING_CREATION_IN_PROGRESS",
"TASK_ORDER_BILLING_CREATION_FAILED",
"TASK_ORDER_BILLING_VERIFICATION_CREATED",
"TASK_ORDER_BILLING_VERIFICATION_IN_PROGRESS",
"TASK_ORDER_BILLING_VERIFICATION_FAILED",
"BILLING_INSTRUCTION_CREATED",
"BILLING_INSTRUCTION_IN_PROGRESS",
"BILLING_INSTRUCTION_FAILED",
"TENANT_PRINCIPAL_APP_CREATED",
"TENANT_PRINCIPAL_APP_IN_PROGRESS",
"TENANT_PRINCIPAL_APP_FAILED",
"TENANT_PRINCIPAL_CREATED",
"TENANT_PRINCIPAL_IN_PROGRESS",
"TENANT_PRINCIPAL_FAILED",
"TENANT_PRINCIPAL_CREDENTIAL_CREATED",
"TENANT_PRINCIPAL_CREDENTIAL_IN_PROGRESS",
"TENANT_PRINCIPAL_CREDENTIAL_FAILED",
"ADMIN_ROLE_DEFINITION_CREATED",
"ADMIN_ROLE_DEFINITION_IN_PROGRESS",
"ADMIN_ROLE_DEFINITION_FAILED",
"PRINCIPAL_ADMIN_ROLE_CREATED",
"PRINCIPAL_ADMIN_ROLE_IN_PROGRESS",
"PRINCIPAL_ADMIN_ROLE_FAILED",
"TENANT_ADMIN_OWNERSHIP_CREATED",
"TENANT_ADMIN_OWNERSHIP_IN_PROGRESS",
"TENANT_ADMIN_OWNERSHIP_FAILED",
"TENANT_PRINCIPAL_OWNERSHIP_CREATED",
"TENANT_PRINCIPAL_OWNERSHIP_IN_PROGRESS",
"TENANT_PRINCIPAL_OWNERSHIP_FAILED",
name="fsmstates",
native_enum=False,
),
existing_nullable=False,
)
# ### end Alembic commands ###

View File

@ -1,12 +1,16 @@
import json import json
import re import re
from secrets import token_urlsafe from secrets import token_urlsafe
from typing import Dict from typing import Any, Dict
from uuid import uuid4 from uuid import uuid4
from atst.utils import sha256_hex
from .cloud_provider_interface import CloudProviderInterface from .cloud_provider_interface import CloudProviderInterface
from .exceptions import AuthenticationException from .exceptions import AuthenticationException
from .models import ( from .models import (
AdminRoleDefinitionCSPPayload,
AdminRoleDefinitionCSPResult,
ApplicationCSPPayload, ApplicationCSPPayload,
ApplicationCSPResult, ApplicationCSPResult,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
@ -23,26 +27,36 @@ from .models import (
ProductPurchaseCSPResult, ProductPurchaseCSPResult,
ProductPurchaseVerificationCSPPayload, ProductPurchaseVerificationCSPPayload,
ProductPurchaseVerificationCSPResult, ProductPurchaseVerificationCSPResult,
PrincipalAdminRoleCSPPayload,
PrincipalAdminRoleCSPResult,
TaskOrderBillingCreationCSPPayload, TaskOrderBillingCreationCSPPayload,
TaskOrderBillingCreationCSPResult, TaskOrderBillingCreationCSPResult,
TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPPayload,
TaskOrderBillingVerificationCSPResult, TaskOrderBillingVerificationCSPResult,
TenantAdminOwnershipCSPPayload,
TenantAdminOwnershipCSPResult,
TenantCSPPayload, TenantCSPPayload,
TenantCSPResult, TenantCSPResult,
TenantPrincipalAppCSPPayload,
TenantPrincipalAppCSPResult,
TenantPrincipalCredentialCSPPayload,
TenantPrincipalCredentialCSPResult,
TenantPrincipalCSPPayload,
TenantPrincipalCSPResult,
TenantPrincipalOwnershipCSPPayload,
TenantPrincipalOwnershipCSPResult,
) )
from .policy import AzurePolicyManager from .policy import AzurePolicyManager
from atst.utils import sha256_hex
AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD
AZURE_SKU_ID = "?" # probably a static sku specific to ATAT/JEDI
SUBSCRIPTION_ID_REGEX = re.compile( SUBSCRIPTION_ID_REGEX = re.compile(
"subscriptions\/([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})", "subscriptions\/([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})",
re.I, re.I,
) )
# This needs to be a fully pathed role definition identifier, not just a UUID # This needs to be a fully pathed role definition identifier, not just a UUID
# TODO: Extract these from sdk msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
AZURE_SKU_ID = "0001" # probably a static sku specific to ATAT/JEDI
REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00000000-0000-4000-8000-000000000000" REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00000000-0000-4000-8000-000000000000"
AZURE_MANAGEMENT_API = "https://management.azure.com"
class AzureSDKProvider(object): class AzureSDKProvider(object):
@ -54,8 +68,9 @@ class AzureSDKProvider(object):
import azure.identity as identity import azure.identity as identity
from azure.keyvault import secrets from azure.keyvault import secrets
from azure.core import exceptions from azure.core import exceptions
from msrestazure.azure_cloud import (
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD AZURE_PUBLIC_CLOUD,
) # TODO: choose cloud type from config
import adal import adal
import requests import requests
@ -70,7 +85,6 @@ class AzureSDKProvider(object):
self.exceptions = exceptions self.exceptions = exceptions
self.secrets = secrets self.secrets = secrets
self.requests = requests self.requests = requests
# may change to a JEDI cloud
self.cloud = AZURE_PUBLIC_CLOUD self.cloud = AZURE_PUBLIC_CLOUD
@ -82,6 +96,9 @@ class AzureCloudProvider(CloudProviderInterface):
self.secret_key = config["AZURE_SECRET_KEY"] self.secret_key = config["AZURE_SECRET_KEY"]
self.tenant_id = config["AZURE_TENANT_ID"] self.tenant_id = config["AZURE_TENANT_ID"]
self.vault_url = config["AZURE_VAULT_URL"] self.vault_url = config["AZURE_VAULT_URL"]
self.ps_client_id = config["POWERSHELL_CLIENT_ID"]
self.owner_role_def_id = config["AZURE_OWNER_ROLE_DEF_ID"]
self.graph_resource = config["AZURE_GRAPH_RESOURCE"]
if azure_sdk_provider is None: if azure_sdk_provider is None:
self.sdk = AzureSDKProvider() self.sdk = AzureSDKProvider()
@ -91,7 +108,7 @@ class AzureCloudProvider(CloudProviderInterface):
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"]) self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
def set_secret(self, secret_key, secret_value): def set_secret(self, secret_key, secret_value):
credential = self._get_client_secret_credential_obj({}) credential = self._get_client_secret_credential_obj()
secret_client = self.sdk.secrets.SecretClient( secret_client = self.sdk.secrets.SecretClient(
vault_url=self.vault_url, credential=credential, vault_url=self.vault_url, credential=credential,
) )
@ -104,7 +121,7 @@ class AzureCloudProvider(CloudProviderInterface):
) )
def get_secret(self, secret_key): def get_secret(self, secret_key):
credential = self._get_client_secret_credential_obj({}) credential = self._get_client_secret_credential_obj()
secret_client = self.sdk.secrets.SecretClient( secret_client = self.sdk.secrets.SecretClient(
vault_url=self.vault_url, credential=credential, vault_url=self.vault_url, credential=credential,
) )
@ -180,7 +197,7 @@ class AzureCloudProvider(CloudProviderInterface):
"secret_key": creds.root_sp_key, "secret_key": creds.root_sp_key,
"tenant_id": creds.root_tenant_id, "tenant_id": creds.root_tenant_id,
}, },
resource=AZURE_MANAGEMENT_API, resource=self.sdk.cloud.endpoints.resource_manager,
) )
response = self._create_management_group( response = self._create_management_group(
@ -305,7 +322,7 @@ class AzureCloudProvider(CloudProviderInterface):
) )
def create_tenant(self, payload: TenantCSPPayload): def create_tenant(self, payload: TenantCSPPayload):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException("Could not resolve token for tenant creation") raise AuthenticationException("Could not resolve token for tenant creation")
@ -317,26 +334,33 @@ class AzureCloudProvider(CloudProviderInterface):
} }
result = self.sdk.requests.post( result = self.sdk.requests.post(
"https://management.azure.com/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview", f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview",
json=create_tenant_body, json=create_tenant_body,
headers=create_tenant_headers, headers=create_tenant_headers,
) )
if result.status_code == 200: if result.status_code == 200:
return self._ok( result_dict = result.json()
TenantCSPResult( tenant_id = result_dict.get("tenantId")
**result.json(), tenant_admin_username = (
f"{payload.user_id}@{payload.domain_name}.onmicrosoft.com"
)
self.update_tenant_creds(
tenant_id,
KeyVaultCredentials(
tenant_id=tenant_id,
tenant_admin_username=tenant_admin_username,
tenant_admin_password=payload.password, tenant_admin_password=payload.password,
tenant_admin_username=payload.user_id, ),
)
) )
return self._ok(TenantCSPResult(**result_dict))
else: else:
return self._error(result.json()) return self._error(result.json())
def create_billing_profile_creation( def create_billing_profile_creation(
self, payload: BillingProfileCreationCSPPayload self, payload: BillingProfileCreationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for billing profile creation" "Could not resolve token for billing profile creation"
@ -348,7 +372,7 @@ class AzureCloudProvider(CloudProviderInterface):
"Authorization": f"Bearer {sp_token}", "Authorization": f"Bearer {sp_token}",
} }
billing_account_create_url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles?api-version=2019-10-01-preview" billing_account_create_url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles?api-version=2019-10-01-preview"
result = self.sdk.requests.post( result = self.sdk.requests.post(
billing_account_create_url, billing_account_create_url,
@ -368,7 +392,7 @@ class AzureCloudProvider(CloudProviderInterface):
def create_billing_profile_verification( def create_billing_profile_verification(
self, payload: BillingProfileVerificationCSPPayload self, payload: BillingProfileVerificationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for billing profile validation" "Could not resolve token for billing profile validation"
@ -393,7 +417,7 @@ class AzureCloudProvider(CloudProviderInterface):
def create_billing_profile_tenant_access( def create_billing_profile_tenant_access(
self, payload: BillingProfileTenantAccessCSPPayload self, payload: BillingProfileTenantAccessCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
request_body = { request_body = {
"properties": { "properties": {
"principalTenantId": payload.tenant_id, # from tenant creation "principalTenantId": payload.tenant_id, # from tenant creation
@ -406,7 +430,7 @@ class AzureCloudProvider(CloudProviderInterface):
"Authorization": f"Bearer {sp_token}", "Authorization": f"Bearer {sp_token}",
} }
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/createBillingRoleAssignment?api-version=2019-10-01-preview" url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/createBillingRoleAssignment?api-version=2019-10-01-preview"
result = self.sdk.requests.post(url, headers=headers, json=request_body) result = self.sdk.requests.post(url, headers=headers, json=request_body)
if result.status_code == 201: if result.status_code == 201:
@ -417,12 +441,12 @@ class AzureCloudProvider(CloudProviderInterface):
def create_task_order_billing_creation( def create_task_order_billing_creation(
self, payload: TaskOrderBillingCreationCSPPayload self, payload: TaskOrderBillingCreationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
request_body = [ request_body = [
{ {
"op": "replace", "op": "replace",
"path": "/enabledAzurePlans", "path": "/enabledAzurePlans",
"value": [{"skuId": "0001"}], "value": [{"skuId": AZURE_SKU_ID}],
} }
] ]
@ -430,7 +454,7 @@ class AzureCloudProvider(CloudProviderInterface):
"Authorization": f"Bearer {sp_token}", "Authorization": f"Bearer {sp_token}",
} }
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}?api-version=2019-10-01-preview" url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}?api-version=2019-10-01-preview"
result = self.sdk.requests.patch( result = self.sdk.requests.patch(
url, headers=request_headers, json=request_body url, headers=request_headers, json=request_body
@ -447,7 +471,7 @@ class AzureCloudProvider(CloudProviderInterface):
def create_task_order_billing_verification( def create_task_order_billing_verification(
self, payload: TaskOrderBillingVerificationCSPPayload self, payload: TaskOrderBillingVerificationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for task order billing validation" "Could not resolve token for task order billing validation"
@ -470,7 +494,7 @@ class AzureCloudProvider(CloudProviderInterface):
return self._error(result.json()) return self._error(result.json())
def create_billing_instruction(self, payload: BillingInstructionCSPPayload): def create_billing_instruction(self, payload: BillingInstructionCSPPayload):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for task order billing validation" "Could not resolve token for task order billing validation"
@ -484,7 +508,7 @@ class AzureCloudProvider(CloudProviderInterface):
} }
} }
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/instructions/{payload.initial_task_order_id}:CLIN00{payload.initial_clin_type}?api-version=2019-10-01-preview" url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/instructions/{payload.initial_task_order_id}:CLIN00{payload.initial_clin_type}?api-version=2019-10-01-preview"
auth_header = { auth_header = {
"Authorization": f"Bearer {sp_token}", "Authorization": f"Bearer {sp_token}",
@ -498,7 +522,7 @@ class AzureCloudProvider(CloudProviderInterface):
return self._error(result.json()) return self._error(result.json())
def create_product_purchase(self, payload: ProductPurchaseCSPPayload): def create_product_purchase(self, payload: ProductPurchaseCSPPayload):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for aad premium product purchase" "Could not resolve token for aad premium product purchase"
@ -540,7 +564,7 @@ class AzureCloudProvider(CloudProviderInterface):
def create_product_purchase_verification( def create_product_purchase_verification(
self, payload: ProductPurchaseVerificationCSPPayload self, payload: ProductPurchaseVerificationCSPPayload
): ):
sp_token = self._get_sp_token(payload.creds) sp_token = self._get_root_provisioning_token()
if sp_token is None: if sp_token is None:
raise AuthenticationException( raise AuthenticationException(
"Could not resolve token for aad premium product purchase validation" "Could not resolve token for aad premium product purchase validation"
@ -567,21 +591,198 @@ class AzureCloudProvider(CloudProviderInterface):
else: else:
return self._error(result.json()) return self._error(result.json())
def create_remote_admin(self, creds, tenant_details): def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload):
# create app/service principal within tenant, with name constructed from tenant details mgmt_token = self._get_elevated_management_token(payload.tenant_id)
# assign principal global admin
# needs to call out to CLI with tenant owner username/password, prototyping for that underway role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}"
# return identifier and creds to consumer for storage request_body = {
response = {"clientId": "string", "secretKey": "string", "tenantId": "string"} "properties": {
return self._ok( "roleDefinitionId": role_definition_id,
{ "principalId": payload.user_object_id,
"client_id": response["clientId"],
"secret_key": response["secret_key"],
"tenant_id": response["tenantId"],
} }
}
auth_header = {
"Authorization": f"Bearer {mgmt_token}",
}
assignment_guid = str(uuid4())
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01"
response = self.sdk.requests.put(url, headers=auth_header, json=request_body)
if response.ok:
return TenantAdminOwnershipCSPResult(**response.json())
def create_tenant_principal_ownership(
self, payload: TenantPrincipalOwnershipCSPPayload
):
mgmt_token = self._get_elevated_management_token(payload.tenant_id)
# NOTE: the tenant_id is also the id of the root management group, once it is created
role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}"
request_body = {
"properties": {
"roleDefinitionId": role_definition_id,
"principalId": payload.principal_id,
}
}
auth_header = {
"Authorization": f"Bearer {mgmt_token}",
}
assignment_guid = str(uuid4())
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01"
response = self.sdk.requests.put(url, headers=auth_header, json=request_body)
if response.ok:
return TenantPrincipalOwnershipCSPResult(**response.json())
def create_tenant_principal_app(self, payload: TenantPrincipalAppCSPPayload):
graph_token = self._get_tenant_admin_token(
payload.tenant_id, self.graph_resource
) )
if graph_token is None:
raise AuthenticationException(
"Could not resolve graph token for tenant admin"
)
request_body = {"displayName": "ATAT Remote Admin"}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/v1.0/applications"
response = self.sdk.requests.post(url, json=request_body, headers=auth_header)
if response.ok:
return TenantPrincipalAppCSPResult(**response.json())
def create_tenant_principal(self, payload: TenantPrincipalCSPPayload):
graph_token = self._get_tenant_admin_token(
payload.tenant_id, self.graph_resource
)
if graph_token is None:
raise AuthenticationException(
"Could not resolve graph token for tenant admin"
)
request_body = {"appId": payload.principal_app_id}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/beta/servicePrincipals"
response = self.sdk.requests.post(url, json=request_body, headers=auth_header)
if response.ok:
return TenantPrincipalCSPResult(**response.json())
def create_tenant_principal_credential(
self, payload: TenantPrincipalCredentialCSPPayload
):
graph_token = self._get_tenant_admin_token(
payload.tenant_id, self.graph_resource
)
if graph_token is None:
raise AuthenticationException(
"Could not resolve graph token for tenant admin"
)
request_body = {
"passwordCredentials": [{"displayName": "ATAT Generated Password"}]
}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/v1.0/applications/{payload.principal_app_object_id}/addPassword"
response = self.sdk.requests.post(url, json=request_body, headers=auth_header)
if response.ok:
result = response.json()
self.update_tenant_creds(
payload.tenant_id,
KeyVaultCredentials(
tenant_id=payload.tenant_id,
tenant_sp_key=result.get("secretText"),
tenant_sp_client_id=payload.principal_app_id,
),
)
return TenantPrincipalCredentialCSPResult(
principal_client_id=payload.principal_app_id,
principal_creds_established=True,
)
def create_admin_role_definition(self, payload: AdminRoleDefinitionCSPPayload):
graph_token = self._get_tenant_admin_token(
payload.tenant_id, self.graph_resource
)
if graph_token is None:
raise AuthenticationException(
"Could not resolve graph token for tenant admin"
)
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/beta/roleManagement/directory/roleDefinitions"
response = self.sdk.requests.get(url, headers=auth_header)
result = response.json()
roleList = result.get("value")
DEFAULT_ADMIN_RD_ID = "794bb258-3e31-42ff-9ee4-731a72f62851"
admin_role_def_id = next(
(
role.get("id")
for role in roleList
if role.get("displayName") == "Company Administrator"
),
DEFAULT_ADMIN_RD_ID,
)
return AdminRoleDefinitionCSPResult(admin_role_def_id=admin_role_def_id)
def create_principal_admin_role(self, payload: PrincipalAdminRoleCSPPayload):
graph_token = self._get_tenant_admin_token(
payload.tenant_id, self.graph_resource
)
if graph_token is None:
raise AuthenticationException(
"Could not resolve graph token for tenant admin"
)
request_body = {
"principalId": payload.principal_id,
"roleDefinitionId": payload.admin_role_def_id,
"resourceScope": "/",
}
auth_header = {
"Authorization": f"Bearer {graph_token}",
}
url = f"{self.graph_resource}/beta/roleManagement/directory/roleAssignments"
response = self.sdk.requests.post(url, headers=auth_header, json=request_body)
if response.ok:
return PrincipalAdminRoleCSPResult(**response.json())
def force_tenant_admin_pw_update(self, creds, tenant_owner_id): def force_tenant_admin_pw_update(self, creds, tenant_owner_id):
# use creds to update to force password recovery? # use creds to update to force password recovery?
@ -651,22 +852,42 @@ class AzureCloudProvider(CloudProviderInterface):
if sub_id_match: if sub_id_match:
return sub_id_match.group(1) return sub_id_match.group(1)
def _get_sp_token(self, creds): def _get_tenant_admin_token(self, tenant_id, resource):
home_tenant_id = creds.get("home_tenant_id") creds = self._source_tenant_creds(tenant_id)
client_id = creds.get("client_id") return self._get_up_token_for_resource(
secret_key = creds.get("secret_key") creds.tenant_admin_username,
creds.tenant_admin_password,
tenant_id,
resource,
)
# TODO: Make endpoints consts or configs def _get_root_provisioning_token(self):
authentication_endpoint = "https://login.microsoftonline.com/" creds = self._source_creds()
resource = "https://management.azure.com/" return self._get_sp_token(
creds.tenant_id, creds.root_sp_client_id, creds.root_sp_key
)
def _get_sp_token(self, tenant_id, client_id, secret_key):
context = self.sdk.adal.AuthenticationContext( context = self.sdk.adal.AuthenticationContext(
authentication_endpoint + home_tenant_id f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
) )
# TODO: handle failure states here # TODO: handle failure states here
token_response = context.acquire_token_with_client_credentials( token_response = context.acquire_token_with_client_credentials(
resource, client_id, secret_key self.sdk.cloud.endpoints.resource_manager, client_id, secret_key
)
return token_response.get("accessToken", None)
def _get_up_token_for_resource(self, username, password, tenant_id, resource):
context = self.sdk.adal.AuthenticationContext(
f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
)
# TODO: handle failure states here
token_response = context.acquire_token_with_username_password(
resource, username, password, self.ps_client_id
) )
return token_response.get("accessToken", None) return token_response.get("accessToken", None)
@ -680,16 +901,14 @@ class AzureCloudProvider(CloudProviderInterface):
cloud_environment=self.sdk.cloud, cloud_environment=self.sdk.cloud,
) )
def _get_client_secret_credential_obj(self, creds): def _get_client_secret_credential_obj(self):
creds = self._source_creds()
return self.sdk.identity.ClientSecretCredential( return self.sdk.identity.ClientSecretCredential(
tenant_id=creds.get("tenant_id"), tenant_id=creds.tenant_id,
client_id=creds.get("client_id"), client_id=creds.root_sp_client_id,
client_secret=creds.get("secret_key"), client_secret=creds.root_sp_key,
) )
def _make_tenant_admin_cred_obj(self, username, password):
return self.sdk.credentials.UserPassCredentials(username, password)
def _ok(self, body=None): def _ok(self, body=None):
return self._make_response("ok", body) return self._make_response("ok", body)
@ -716,6 +935,26 @@ class AzureCloudProvider(CloudProviderInterface):
"tenant_id": self.tenant_id, "tenant_id": self.tenant_id,
} }
def _get_elevated_management_token(self, tenant_id):
mgmt_token = self._get_tenant_admin_token(
tenant_id, self.sdk.cloud.endpoints.resource_manager
)
if mgmt_token is None:
raise AuthenticationException(
"Failed to resolve management token for tenant admin"
)
auth_header = {
"Authorization": f"Bearer {mgmt_token}",
}
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"
result = self.sdk.requests.post(url, headers=auth_header)
if not result.ok:
raise AuthenticationException("Failed to elevate access")
return mgmt_token
def _source_creds(self, tenant_id=None) -> KeyVaultCredentials: def _source_creds(self, tenant_id=None) -> KeyVaultCredentials:
if tenant_id: if tenant_id:
return self._source_tenant_creds(tenant_id) return self._source_tenant_creds(tenant_id)
@ -726,13 +965,16 @@ class AzureCloudProvider(CloudProviderInterface):
root_sp_key=self._root_creds.get("secret_key"), root_sp_key=self._root_creds.get("secret_key"),
) )
def update_tenant_creds(self, tenant_id, secret): def update_tenant_creds(self, tenant_id, secret: KeyVaultCredentials):
hashed = sha256_hex(tenant_id) hashed = sha256_hex(tenant_id)
self.set_secret(hashed, json.dumps(secret)) new_secrets = secret.dict()
curr_secrets = self._source_tenant_creds(tenant_id)
updated_secrets: Dict[str, Any] = {**curr_secrets.dict(), **new_secrets}
us = KeyVaultCredentials(**updated_secrets)
self.set_secret(hashed, json.dumps(us.dict()))
return us
return secret def _source_tenant_creds(self, tenant_id) -> KeyVaultCredentials:
def _source_tenant_creds(self, tenant_id):
hashed = sha256_hex(tenant_id) hashed = sha256_hex(tenant_id)
raw_creds = self.get_secret(hashed) raw_creds = self.get_secret(hashed)
return KeyVaultCredentials(**json.loads(raw_creds)) return KeyVaultCredentials(**json.loads(raw_creds))

View File

@ -1,41 +1,52 @@
from uuid import uuid4 from uuid import uuid4
from atst.domain.csp.cloud.exceptions import (
BaselineProvisionException,
EnvironmentCreationException,
GeneralCSPException,
UserProvisioningException,
UserRemovalException,
)
from atst.domain.csp.cloud.models import BillingProfileTenantAccessCSPResult
from .cloud_provider_interface import CloudProviderInterface from .cloud_provider_interface import CloudProviderInterface
from .exceptions import ( from .exceptions import (
AuthenticationException, AuthenticationException,
AuthorizationException, AuthorizationException,
BaselineProvisionException,
ConnectionException, ConnectionException,
EnvironmentCreationException,
GeneralCSPException,
UnknownServerException, UnknownServerException,
UserProvisioningException,
UserRemovalException,
) )
from .models import ( from .models import (
AZURE_MGMNT_PATH, AZURE_MGMNT_PATH,
AdminRoleDefinitionCSPPayload,
AdminRoleDefinitionCSPResult,
ApplicationCSPPayload, ApplicationCSPPayload,
ApplicationCSPResult, ApplicationCSPResult,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
BillingInstructionCSPResult, BillingInstructionCSPResult,
BillingProfileCreationCSPPayload, BillingProfileCreationCSPPayload,
BillingProfileCreationCSPResult, BillingProfileCreationCSPResult,
BillingProfileTenantAccessCSPResult,
BillingProfileVerificationCSPPayload, BillingProfileVerificationCSPPayload,
BillingProfileVerificationCSPResult, BillingProfileVerificationCSPResult,
ProductPurchaseCSPPayload, ProductPurchaseCSPPayload,
ProductPurchaseCSPResult, ProductPurchaseCSPResult,
ProductPurchaseVerificationCSPPayload, ProductPurchaseVerificationCSPPayload,
ProductPurchaseVerificationCSPResult, ProductPurchaseVerificationCSPResult,
PrincipalAdminRoleCSPPayload,
PrincipalAdminRoleCSPResult,
TaskOrderBillingCreationCSPPayload, TaskOrderBillingCreationCSPPayload,
TaskOrderBillingCreationCSPResult, TaskOrderBillingCreationCSPResult,
TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPPayload,
TaskOrderBillingVerificationCSPResult, TaskOrderBillingVerificationCSPResult,
TenantAdminOwnershipCSPPayload,
TenantAdminOwnershipCSPResult,
TenantCSPPayload, TenantCSPPayload,
TenantCSPResult, TenantCSPResult,
TenantPrincipalAppCSPPayload,
TenantPrincipalAppCSPResult,
TenantPrincipalCredentialCSPPayload,
TenantPrincipalCredentialCSPResult,
TenantPrincipalCSPPayload,
TenantPrincipalCSPResult,
TenantPrincipalOwnershipCSPPayload,
TenantPrincipalOwnershipCSPResult,
) )
@ -124,7 +135,7 @@ class MockCloudProvider(CloudProviderInterface):
payload is an instance of TenantCSPPayload data class payload is an instance of TenantCSPPayload data class
""" """
self._authorize(payload.creds) self._authorize("admin")
self._delay(1, 5) self._delay(1, 5)
@ -304,6 +315,70 @@ class MockCloudProvider(CloudProviderInterface):
**dict(premium_purchase_date="2020-01-30T18:57:05.981Z") **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_or_update_user(self, auth_credentials, user_info, csp_role_id): def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
self._authorize(auth_credentials) self._authorize(auth_credentials)

View File

@ -22,20 +22,10 @@ class AliasModel(BaseModel):
class BaseCSPPayload(AliasModel): class BaseCSPPayload(AliasModel):
# {"username": "mock-cloud", "pass": "shh"} tenant_id: str
creds: Dict
def dict(self, *args, **kwargs):
exclude = {"creds"}
if "exclude" not in kwargs:
kwargs["exclude"] = exclude
else:
kwargs["exclude"].update(exclude)
return super().dict(*args, **kwargs)
class TenantCSPPayload(BaseCSPPayload): class TenantCSPPayload(AliasModel):
user_id: str user_id: str
password: Optional[str] password: Optional[str]
domain_name: str domain_name: str
@ -236,6 +226,81 @@ class BillingInstructionCSPResult(AliasModel):
} }
class TenantAdminOwnershipCSPPayload(BaseCSPPayload):
user_object_id: str
class TenantAdminOwnershipCSPResult(AliasModel):
admin_owner_assignment_id: str
class Config:
fields = {"admin_owner_assignment_id": "id"}
class TenantPrincipalOwnershipCSPPayload(BaseCSPPayload):
principal_id: str
class TenantPrincipalOwnershipCSPResult(AliasModel):
principal_owner_assignment_id: str
class Config:
fields = {"principal_owner_assignment_id": "id"}
class TenantPrincipalAppCSPPayload(BaseCSPPayload):
pass
class TenantPrincipalAppCSPResult(AliasModel):
principal_app_id: str
principal_app_object_id: str
class Config:
fields = {"principal_app_id": "appId", "principal_app_object_id": "id"}
class TenantPrincipalCSPPayload(BaseCSPPayload):
principal_app_id: str
class TenantPrincipalCSPResult(AliasModel):
principal_id: str
class Config:
fields = {"principal_id": "id"}
class TenantPrincipalCredentialCSPPayload(BaseCSPPayload):
principal_app_id: str
principal_app_object_id: str
class TenantPrincipalCredentialCSPResult(AliasModel):
principal_client_id: str
principal_creds_established: bool
class AdminRoleDefinitionCSPPayload(BaseCSPPayload):
pass
class AdminRoleDefinitionCSPResult(AliasModel):
admin_role_def_id: str
class PrincipalAdminRoleCSPPayload(BaseCSPPayload):
principal_id: str
admin_role_def_id: str
class PrincipalAdminRoleCSPResult(AliasModel):
principal_assignment_id: str
class Config:
fields = {"principal_assignment_id": "id"}
AZURE_MGMNT_PATH = "/providers/Microsoft.Management/managementGroups/" AZURE_MGMNT_PATH = "/providers/Microsoft.Management/managementGroups/"
MANAGEMENT_GROUP_NAME_REGEX = "^[a-zA-Z0-9\-_\(\)\.]+$" MANAGEMENT_GROUP_NAME_REGEX = "^[a-zA-Z0-9\-_\(\)\.]+$"

View File

@ -19,6 +19,13 @@ class AzureStages(Enum):
BILLING_INSTRUCTION = "billing instruction" BILLING_INSTRUCTION = "billing instruction"
PRODUCT_PURCHASE = "purchase aad premium product" PRODUCT_PURCHASE = "purchase aad premium product"
PRODUCT_PURCHASE_VERIFICATION = "purchase aad premium product verification" PRODUCT_PURCHASE_VERIFICATION = "purchase aad premium product verification"
TENANT_PRINCIPAL_APP = "tenant principal application"
TENANT_PRINCIPAL = "tenant principal"
TENANT_PRINCIPAL_CREDENTIAL = "tenant principal credential"
ADMIN_ROLE_DEFINITION = "admin role definition"
PRINCIPAL_ADMIN_ROLE = "tenant principal admin"
TENANT_ADMIN_OWNERSHIP = "tenant admin ownership"
TENANT_PRINCIPAL_OWNERSHIP = "tenant principial ownership"
def _build_csp_states(csp_stages): def _build_csp_states(csp_stages):

View File

@ -168,14 +168,6 @@ class PortfolioStateMachine(
self.portfolio.csp_data.update(response.dict()) self.portfolio.csp_data.update(response.dict())
db.session.add(self.portfolio) db.session.add(self.portfolio)
db.session.commit() db.session.commit()
if getattr(response, "get_creds", None) is not None:
new_creds = response.get_creds()
# TODO: one way salted hash of tenant_id to use as kv key name?
tenant_id = new_creds.get("tenant_id")
secret = self.csp.get_secret(tenant_id, new_creds)
secret.update(new_creds)
self.csp.update_tenant_creds(tenant_id, secret)
except PydanticValidationError as exc: except PydanticValidationError as exc:
app.logger.error( app.logger.error(
f"Failed to cast response to valid result class {self.__repr__()}:", f"Failed to cast response to valid result class {self.__repr__()}:",

View File

@ -14,7 +14,9 @@ from flask import (
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
import pendulum import pendulum
import os import os
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound, MethodNotAllowed
from werkzeug.routing import RequestRedirect
from atst.domain.users import Users from atst.domain.users import Users
from atst.domain.authnid import AuthenticationContext from atst.domain.authnid import AuthenticationContext
@ -61,17 +63,36 @@ def _make_authentication_context():
def redirect_after_login_url(): def redirect_after_login_url():
if request.args.get("next"):
returl = request.args.get("next") returl = request.args.get("next")
if request.args.get(app.form_cache.PARAM_NAME): if match_url_pattern(returl):
returl += "?" + url.urlencode( param_name = request.args.get(app.form_cache.PARAM_NAME)
{app.form_cache.PARAM_NAME: request.args.get(app.form_cache.PARAM_NAME)} if param_name:
) returl += "?" + url.urlencode({app.form_cache.PARAM_NAME: param_name})
return returl return returl
else: else:
return url_for("atst.home") return url_for("atst.home")
def match_url_pattern(url, method="GET"):
"""Ensure a url matches a url pattern in the flask app
inspired by https://stackoverflow.com/questions/38488134/get-the-flask-view-function-that-matches-a-url/38488506#38488506
"""
server_name = app.config.get("SERVER_NAME") or "localhost"
adapter = app.url_map.bind(server_name=server_name)
try:
match = adapter.match(url, method=method)
except RequestRedirect as e:
# recursively match redirects
return match_url_pattern(e.new_url, method)
except (MethodNotAllowed, NotFound):
# no match
return None
if match[0] in app.view_functions:
return url
def current_user_setup(user): def current_user_setup(user):
session["user_id"] = user.id session["user_id"] = user.id
session["last_login"] = user.last_login session["last_login"] = user.last_login
@ -109,13 +130,3 @@ def logout():
@bp.route("/about") @bp.route("/about")
def about(): def about():
return render_template("about.html") return render_template("about.html")
@bp.route("/csp-environment-access")
def csp_environment_access():
return render_template("mock_csp.html", text="console for this environment")
@bp.route("/jedi-csp-calculator")
def jedi_csp_calculator():
return redirect(app.csp.cloud.get_calculator_url())

View File

@ -22,9 +22,4 @@ def wrap_environment_role_lookup(user, environment_id=None, **kwargs):
@applications_bp.route("/environments/<environment_id>/access") @applications_bp.route("/environments/<environment_id>/access")
@user_can(None, override=wrap_environment_role_lookup, message="access environment") @user_can(None, override=wrap_environment_role_lookup, message="access environment")
def access_environment(environment_id): def access_environment(environment_id):
env_role = EnvironmentRoles.get_by_user_and_environment( return redirect("https://portal.azure.com")
g.current_user.id, environment_id
)
login_url = app.csp.cloud.get_environment_login_url(env_role.environment)
return redirect(url_for("atst.csp_environment_access", login_url=login_url))

View File

@ -70,7 +70,12 @@ def update_task_order(form, portfolio_id=None, task_order_id=None, flash_invalid
def update_and_render_next( def update_and_render_next(
form_data, next_page, current_template, portfolio_id=None, task_order_id=None form_data,
next_page,
current_template,
portfolio_id=None,
task_order_id=None,
previous=False,
): ):
form = None form = None
if task_order_id: if task_order_id:
@ -80,8 +85,9 @@ def update_and_render_next(
form = TaskOrderForm(form_data) form = TaskOrderForm(form_data)
task_order = update_task_order(form, portfolio_id, task_order_id) task_order = update_task_order(form, portfolio_id, task_order_id)
if task_order: if task_order or previous:
return redirect(url_for(next_page, task_order_id=task_order.id)) to_id = task_order.id if task_order else task_order_id
return redirect(url_for(next_page, task_order_id=to_id))
else: else:
return ( return (
render_task_orders_edit( render_task_orders_edit(
@ -210,12 +216,21 @@ def form_step_two_add_number(task_order_id):
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_2", methods=["POST"]) @task_orders_bp.route("/task_orders/<task_order_id>/form/step_2", methods=["POST"])
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form") @user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
def submit_form_step_two_add_number(task_order_id): def submit_form_step_two_add_number(task_order_id):
previous = http_request.args.get("previous", "False").lower() == "true"
form_data = {**http_request.form} form_data = {**http_request.form}
next_page = "task_orders.form_step_three_add_clins" next_page = (
"task_orders.form_step_three_add_clins"
if not previous
else "task_orders.form_step_one_add_pdf"
)
current_template = "task_orders/step_2.html" current_template = "task_orders/step_2.html"
return update_and_render_next( return update_and_render_next(
form_data, next_page, current_template, task_order_id=task_order_id form_data,
next_page,
current_template,
task_order_id=task_order_id,
previous=previous,
) )
@ -230,12 +245,21 @@ def form_step_three_add_clins(task_order_id):
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_3", methods=["POST"]) @task_orders_bp.route("/task_orders/<task_order_id>/form/step_3", methods=["POST"])
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form") @user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
def submit_form_step_three_add_clins(task_order_id): def submit_form_step_three_add_clins(task_order_id):
previous = http_request.args.get("previous", "False").lower() == "true"
form_data = {**http_request.form} form_data = {**http_request.form}
next_page = "task_orders.form_step_four_review" next_page = (
"task_orders.form_step_four_review"
if not previous
else "task_orders.form_step_two_add_number"
)
current_template = "task_orders/step_3.html" current_template = "task_orders/step_3.html"
return update_and_render_next( return update_and_render_next(
form_data, next_page, current_template, task_order_id=task_order_id form_data,
next_page,
current_template,
task_order_id=task_order_id,
previous=previous,
) )

View File

@ -3,6 +3,7 @@ from flask import Blueprint, render_template, g, request as http_request, redire
from atst.forms.edit_user import EditUserForm from atst.forms.edit_user import EditUserForm
from atst.domain.users import Users from atst.domain.users import Users
from atst.utils.flash import formatted_flash as flash from atst.utils.flash import formatted_flash as flash
from atst.routes import match_url_pattern
bp = Blueprint("users", __name__) bp = Blueprint("users", __name__)
@ -35,7 +36,7 @@ def update_user():
if form.validate(): if form.validate():
Users.update(user, form.data) Users.update(user, form.data)
flash("user_updated") flash("user_updated")
if next_url: if match_url_pattern(next_url):
return redirect(next_url) return redirect(next_url)
return render_template( return render_template(

View File

@ -43,6 +43,7 @@ SERVER_NAME
SESSION_COOKIE_NAME=atat SESSION_COOKIE_NAME=atat
SESSION_COOKIE_DOMAIN SESSION_COOKIE_DOMAIN
SESSION_KEY_PREFIX=session: SESSION_KEY_PREFIX=session:
SESSION_COOKIE_SECURE=false
SESSION_TYPE = redis SESSION_TYPE = redis
SESSION_USE_SIGNER = True SESSION_USE_SIGNER = True
SQLALCHEMY_ECHO = False SQLALCHEMY_ECHO = False

View File

@ -32,6 +32,7 @@ data:
REDIS_HOST: atat.redis.cache.windows.net:6380 REDIS_HOST: atat.redis.cache.windows.net:6380
REDIS_TLS: "true" REDIS_TLS: "true"
SESSION_COOKIE_DOMAIN: atat.code.mil SESSION_COOKIE_DOMAIN: atat.code.mil
SESSION_COOKIE_SECURE: "true"
STATIC_URL: https://atat-cdn.azureedge.net/static/ STATIC_URL: https://atat-cdn.azureedge.net/static/
TZ: UTC TZ: UTC
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini

View File

@ -10,7 +10,7 @@ data:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
# Set SSL protocols, ciphers, and related options # Set SSL protocols, ciphers, and related options
ssl_protocols TLSv1.3 TLSv1.2; ssl_protocols TLSv1.3 TLSv1.2;
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:!EXPORT;
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
ssl_ecdh_curve X25519:prime256v1:secp384r1; ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_dhparam /etc/ssl/dhparam.pem; ssl_dhparam /etc/ssl/dhparam.pem;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -1,65 +0,0 @@
<label for="toggle">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="100" height="100">
<path fill="white" id="cloud_icon" d="M11.5 15H5c-2.757 0-5-2.243-5-5 0-2.204 1.446-4.126 3.511-4.769C4.395 3.277 6.335 2 8.5 2c2.773 0 5.058 2.034 5.434 4.727C15.205 7.548 16 8.972 16 10.5c0 2.481-2.019 4.5-4.5 4.5zm-3-11c-1.493 0-2.819.962-3.3 2.394-.115.342-.407.596-.762.664C3.025 7.326 2 8.563 2 10c0 1.654 1.346 3 3 3h6.5c1.379 0 2.5-1.121 2.5-2.5 0-.972-.553-1.835-1.441-2.254-.339-.16-.561-.495-.573-.869C11.918 5.483 10.387 4 8.5 4z"/>
</svg>
</label>
<input type="checkbox" id="toggle" />
<div class="main">
<p align="center" >Once the Cloud Service Provider is selected, this link will take you to the CSP's {{ text }}!</p>
</div>
<style>
html, body {
height: 100%;
margin: 0px;
}
.main p {
font-family: "Source Sans Pro", sans-serif;
margin: 1.6rem 0;
font-size: 1.8rem;
font-weight: 700;
color: black;
position: fixed;
top: 9em;
width: 70%;
left: 15%;
}
/* Toggle Switch */
label {
display: block;
height: 100px;
width: 100px;
margin: 20px 0;
position: fixed;
left: 50%;
margin-left: -50px;
top: 8em;
}
input#toggle {
position: absolute;
top: -9999px;
left: -9999px;
}
/* Wrapper */
.main {
width: 100%;
height: 100%;
background: url(static/img/cloud_background.jpg);
background-size: cover;
}
/* Toggle Background Image */
input#toggle:checked + .main {
background: url(static/img/cloud-background-2.gif);
background-size: cover;
}
input#toggle:checked + .main p {
color: white;
}
</style>

View File

@ -14,10 +14,14 @@
{% call Modal(name='cancel', dismissable=True) %} {% call Modal(name='cancel', dismissable=True) %}
<div class="task-order__modal-cancel"> <div class="task-order__modal-cancel">
<h1>Do you want to save this draft?</h1> <h1>{{ 'task_orders.form.builder_base.cancel_modal' | translate }}</h1>
<div class="action-group"> <div class="action-group">
<button formaction="{{ cancel_discard_url }}" class="usa-button usa-button-primary" type="submit">No, delete it</button> <button formaction="{{ cancel_discard_url }}" class="usa-button usa-button-primary" type="submit">
<button formaction="{{ cancel_save_url }}" class="usa-button usa-button-primary" type="submit">Yes, save for later</button> {{ "task_orders.form.builder_base.delete_draft" | translate }}
</button>
<button formaction="{{ cancel_save_url }}" class="usa-button usa-button-primary" type="submit">
{{ "task_orders.form.builder_base.save_draft" | translate }}
</button>
</div> </div>
</div> </div>
{% endcall %} {% endcall %}
@ -39,9 +43,20 @@
{% endblock %} {% endblock %}
{% if step != "1" %} {% if step != "1" %}
<a class="usa-button usa-button-secondary" href="{{ previous_button_link }}"> {% if step == "2" or step == "3" -%}
Previous <button
type="submit"
class="usa-button usa-button-secondary"
formaction="{{ previous_button_link }}">
{{ "common.previous" | translate }}
</button>
{% else -%}
<a
class="usa-button usa-button-secondary"
href="{{ previous_button_link }}">
{{ "common.previous" | translate }}
</a> </a>
{%- endif %}
{% endif %} {% endif %}
<a <a

View File

@ -7,7 +7,7 @@
{%- endif %} {%- endif %}
{% if to_number %} {% if to_number %}
<p> <p>
<strong>Task Order Number:</strong> {{ to_number }} {{ "task_orders.form.builder_base.to_number" | translate({ "number": to_number }) | safe }}
</p> </p>
{% endif %} {% endif %}
{% if description %} {% if description %}

View File

@ -10,7 +10,7 @@
{% set action = url_for("task_orders.submit_form_step_one_add_pdf", portfolio_id=portfolio.id) %} {% set action = url_for("task_orders.submit_form_step_one_add_pdf", portfolio_id=portfolio.id) %}
{% endif %} {% endif %}
{% set next_button_text = "Next: Add TO Number" %} {% set next_button_text = "task_orders.form.step_1.next_button" | translate %}
{% set step = "1" %} {% set step = "1" %}
{% set sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %} {% set sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %}

View File

@ -4,8 +4,8 @@
{% from "task_orders/form_header.html" import TOFormStepHeader %} {% from "task_orders/form_header.html" import TOFormStepHeader %}
{% set action = url_for("task_orders.submit_form_step_two_add_number", task_order_id=task_order_id) %} {% set action = url_for("task_orders.submit_form_step_two_add_number", task_order_id=task_order_id) %}
{% set next_button_text = "Next: Add Base CLIN" %} {% set next_button_text = "task_orders.form.step_2.next_button" | translate %}
{% set previous_button_link = url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order_id) %} {% set previous_button_link = url_for("task_orders.submit_form_step_two_add_number", task_order_id=task_order_id, previous=True) %}
{% set step = "2" %} {% set step = "2" %}
{% set sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %} {% set sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %}

View File

@ -6,7 +6,7 @@
{% set action = url_for("task_orders.submit_form_step_three_add_clins", task_order_id=task_order_id) %} {% set action = url_for("task_orders.submit_form_step_three_add_clins", task_order_id=task_order_id) %}
{% set next_button_text = "task_orders.form.step_3.next_button" | translate %} {% set next_button_text = "task_orders.form.step_3.next_button" | translate %}
{% set previous_button_link = url_for("task_orders.form_step_two_add_number", task_order_id=task_order_id) %} {% set previous_button_link = url_for("task_orders.submit_form_step_three_add_clins", task_order_id=task_order_id, previous=True) %}
{% set step = "3" %} {% set step = "3" %}
{% set sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %} {% set sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %}

View File

@ -12,7 +12,7 @@
<a <a
href="{{ action }}" href="{{ action }}"
class="usa-button usa-button-primary"> class="usa-button usa-button-primary">
Next: Confirm {{ "task_orders.form.step_4.next_button" | translate }}
</a> </a>
{% endblock %} {% endblock %}

View File

@ -30,3 +30,11 @@ resource "azurerm_storage_container" "bucket" {
storage_account_name = azurerm_storage_account.bucket.name storage_account_name = azurerm_storage_account.bucket.name
container_access_type = var.container_access_type container_access_type = var.container_access_type
} }
# Added until requisite TF bugs are fixed. Typically this would be configured in the
# storage_account resource
resource "null_resource" "retention" {
provisioner "local-exec" {
command = "az storage logging update --account-name ${azurerm_storage_account.bucket.name} --log rwd --services bqt --retention 90"
}
}

View File

@ -29,3 +29,15 @@ resource "azurerm_cdn_endpoint" "cdn" {
host_name = var.origin_host_name host_name = var.origin_host_name
} }
} }
resource "azurerm_monitor_diagnostic_setting" "acr_diagnostic" {
name = "${var.name}-${var.environment}-acr-diag"
target_resource_id = azurerm_cdn_endpoint.cdn.id
log_analytics_workspace_id = var.workspace_id
log {
category = "CoreAnalytics"
retention_policy {
enabled = true
}
}
}

View File

@ -29,3 +29,7 @@ variable "origin_host_name" {
description = "Subdomain to use for the origin in requests to the CDN" description = "Subdomain to use for the origin in requests to the CDN"
} }
variable "workspace_id" {
description = "Log Analytics Workspace ID for sending logs generated by this resource"
type = string
}

View File

@ -36,8 +36,32 @@ resource "azurerm_container_registry" "acr" {
virtual_network = [ virtual_network = [
for subnet in var.subnet_ids : { for subnet in var.subnet_ids : {
action = "Allow" action = "Allow"
subnet_id = subnet.value subnet_id = subnet
} }
] ]
} }
} }
resource "azurerm_monitor_diagnostic_setting" "acr_diagnostic" {
name = "${var.name}-${var.environment}-acr-diag"
target_resource_id = azurerm_container_registry.acr.id
log_analytics_workspace_id = var.workspace_id
log {
category = "ContainerRegistryRepositoryEvents"
retention_policy {
enabled = true
}
}
log {
category = "ContainerRegistryLoginEvents"
retention_policy {
enabled = true
}
}
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -52,3 +52,8 @@ variable "whitelist" {
description = "A map of whitelisted IPs and CIDR ranges. For single IPs, Azure expects just the IP, NOT a /32." description = "A map of whitelisted IPs and CIDR ranges. For single IPs, Azure expects just the IP, NOT a /32."
default = {} default = {}
} }
variable "workspace_id" {
description = "The Log Analytics Workspace ID"
type = string
}

View File

@ -39,3 +39,45 @@ resource "azurerm_kubernetes_cluster" "k8s" {
owner = var.owner owner = var.owner
} }
} }
resource "azurerm_monitor_diagnostic_setting" "k8s_diagnostic-1" {
name = "${var.name}-${var.environment}-k8s-diag"
target_resource_id = azurerm_kubernetes_cluster.k8s.id
log_analytics_workspace_id = var.workspace_id
log {
category = "kube-apiserver"
retention_policy {
enabled = true
}
}
log {
category = "kube-controller-manager"
retention_policy {
enabled = true
}
}
log {
category = "kube-scheduler"
retention_policy {
enabled = true
}
}
log {
category = "kube-audit"
retention_policy {
enabled = true
}
}
log {
category = "cluster-autoscaler"
retention_policy {
enabled = true
}
}
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -62,3 +62,8 @@ variable "client_secret" {
type = string type = string
description = "The client secret for the Service Principal associated with the AKS cluster." description = "The client secret for the Service Principal associated with the AKS cluster."
} }
variable "workspace_id" {
description = "Log Analytics workspace for this resource to log to"
type = string
}

View File

@ -77,3 +77,25 @@ resource "azurerm_key_vault_access_policy" "keyvault_admin_policy" {
"update", "update",
] ]
} }
resource "azurerm_monitor_diagnostic_setting" "keyvault_diagnostic" {
name = "${var.name}-${var.environment}-keyvault-diag"
target_resource_id = azurerm_key_vault.keyvault.id
log_analytics_workspace_id = var.workspace_id
log {
category = "AuditEvent"
enabled = true
retention_policy {
enabled = true
}
}
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -49,3 +49,9 @@ variable "whitelist" {
description = "A map of whitelisted IPs and CIDR ranges. For single IPs, Azure expects just the IP, NOT a /32." description = "A map of whitelisted IPs and CIDR ranges. For single IPs, Azure expects just the IP, NOT a /32."
default = {} default = {}
} }
variable "workspace_id" {
description = "Log Analytics Workspace ID for sending logs generated by this resource"
type = string
}

View File

@ -0,0 +1,15 @@
resource "azurerm_resource_group" "log_workspace" {
name = "${var.name}-${var.environment}-log-workspace"
location = var.region
}
resource "azurerm_log_analytics_workspace" "log_workspace" {
name = "${var.name}-${var.environment}-log-workspace"
location = azurerm_resource_group.log_workspace.location
resource_group_name = azurerm_resource_group.log_workspace.name
sku = "Premium"
tags = {
environment = var.environment
owner = var.owner
}
}

View File

@ -0,0 +1,3 @@
output "workspace_id" {
value = azurerm_log_analytics_workspace.log_workspace.id
}

View File

@ -0,0 +1,19 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}

View File

@ -35,3 +35,33 @@ resource "azurerm_postgresql_virtual_network_rule" "sql" {
subnet_id = var.subnet_id subnet_id = var.subnet_id
ignore_missing_vnet_service_endpoint = true ignore_missing_vnet_service_endpoint = true
} }
resource "azurerm_postgresql_database" "db" {
name = "${var.name}-${var.environment}-atat"
resource_group_name = azurerm_resource_group.sql.name
server_name = azurerm_postgresql_server.sql.name
charset = "UTF8"
collation = "en-US"
}
resource "azurerm_monitor_diagnostic_setting" "postgresql_diagnostic" {
name = "${var.name}-${var.environment}-postgresql-diag"
target_resource_id = azurerm_postgresql_server.sql.id
log_analytics_workspace_id = var.workspace_id
log {
category = "PostgreSQLLogs"
enabled = true
retention_policy {
enabled = true
}
}
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -93,3 +93,8 @@ variable "ssl_enforcement" {
description = "Enforce SSL (Enabled/Disable)" description = "Enforce SSL (Enabled/Disable)"
default = "Enabled" default = "Enabled"
} }
variable "workspace_id" {
description = "Log Analytics workspace for this resource to log to"
type = string
}

View File

@ -23,3 +23,16 @@ resource "azurerm_redis_cache" "redis" {
owner = var.owner owner = var.owner
} }
} }
resource "azurerm_monitor_diagnostic_setting" "redis_diagnostic" {
name = "${var.name}-${var.environment}-redis-diag"
target_resource_id = azurerm_redis_cache.redis.id
log_analytics_workspace_id = var.workspace_id
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -58,3 +58,8 @@ variable "subnet_id" {
type = string type = string
description = "Subnet ID that the service_endpoint should reside" description = "Subnet ID that the service_endpoint should reside"
} }
variable "workspace_id" {
description = "Log Analytics workspace for this resource to log to"
type = string
}

View File

@ -72,45 +72,3 @@ resource "azurerm_route" "route" {
address_prefix = "0.0.0.0/0" address_prefix = "0.0.0.0/0"
next_hop_type = each.value next_hop_type = each.value
} }
# Required for the gateway
resource "azurerm_subnet" "gateway" {
name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.vpc.name
virtual_network_name = azurerm_virtual_network.vpc.name
address_prefix = var.gateway_subnet
}
resource "azurerm_public_ip" "vpn_ip" {
name = "${var.name}-${var.environment}-vpn-ip"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
allocation_method = "Dynamic"
}
resource "azurerm_virtual_network_gateway" "vnet_gateway" {
name = "${var.name}-${var.environment}-gateway"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "Standard"
ip_configuration {
name = "vnetGatewayConfig"
public_ip_address_id = azurerm_public_ip.vpn_ip.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.gateway.id
}
vpn_client_configuration {
address_space = var.vpn_client_cidr
vpn_client_protocols = ["OpenVPN"]
}
}

View File

@ -34,7 +34,6 @@ variable "networks" {
variable "dns_servers" { variable "dns_servers" {
description = "DNS Server IPs for internal and public DNS lookups (must be on a defined subnet)" description = "DNS Server IPs for internal and public DNS lookups (must be on a defined subnet)"
type = list type = list
} }
variable "route_tables" { variable "route_tables" {
@ -42,19 +41,8 @@ variable "route_tables" {
description = "A map with the route tables to create" description = "A map with the route tables to create"
} }
variable "gateway_subnet" {
type = string
description = "The Subnet CIDR that we'll use for the virtual_network_gateway 'GatewaySubnet'"
}
variable "service_endpoints" { variable "service_endpoints" {
type = map type = map
description = "A map of the service endpoints and its mapping to subnets" description = "A map of the service endpoints and its mapping to subnets"
} }
variable "vpn_client_cidr" {
type = list
description = "The CIDR range used for clients on the VPN"
default = ["172.16.0.0/16"]
}

View File

@ -5,4 +5,5 @@ module "cdn" {
environment = var.environment environment = var.environment
name = var.name name = var.name
region = var.region region = var.region
workspace_id = module.logs.workspace_id
} }

View File

@ -6,6 +6,7 @@ module "container_registry" {
owner = var.owner owner = var.owner
backup_region = var.backup_region backup_region = var.backup_region
policy = "Deny" policy = "Deny"
subnet_ids = [] subnet_ids = [module.vpc.subnet_list["private"].id]
whitelist = var.admin_user_whitelist whitelist = var.admin_user_whitelist
workspace_id = module.logs.workspace_id
} }

View File

@ -22,6 +22,7 @@ module "k8s" {
min_count = 3 min_count = 3
client_id = data.azurerm_key_vault_secret.k8s_client_id.value client_id = data.azurerm_key_vault_secret.k8s_client_id.value
client_secret = data.azurerm_key_vault_secret.k8s_client_secret.value client_secret = data.azurerm_key_vault_secret.k8s_client_secret.value
workspace_id = module.logs.workspace_id
} }
#module "main_lb" { #module "main_lb" {

View File

@ -10,5 +10,6 @@ module "keyvault" {
policy = "Deny" policy = "Deny"
subnet_ids = [module.vpc.subnets] subnet_ids = [module.vpc.subnets]
whitelist = var.admin_user_whitelist whitelist = var.admin_user_whitelist
workspace_id = module.logs.workspace_id
} }

View File

@ -0,0 +1,8 @@
module "logs" {
source = "../../modules/log_analytics"
owner = var.owner
environment = var.environment
region = var.region
name = var.name
}

View File

@ -14,7 +14,8 @@ module "sql" {
owner = var.owner owner = var.owner
environment = var.environment environment = var.environment
region = var.region region = var.region
subnet_id = module.vpc.subnets # FIXME - Should be a map of subnets and specify private subnet_id = module.vpc.subnet_list["private"].id
administrator_login = data.azurerm_key_vault_secret.postgres_username.value administrator_login = data.azurerm_key_vault_secret.postgres_username.value
administrator_login_password = data.azurerm_key_vault_secret.postgres_password.value administrator_login_password = data.azurerm_key_vault_secret.postgres_password.value
workspace_id = module.logs.workspace_id
} }

View File

@ -7,4 +7,5 @@ module "redis" {
subnet_id = module.vpc.subnet_list["redis"].id subnet_id = module.vpc.subnet_list["redis"].id
sku_name = "Premium" sku_name = "Premium"
family = "P" family = "P"
workspace_id = module.logs.workspace_id
} }

View File

@ -10,4 +10,5 @@ module "operator_keyvault" {
policy = "Deny" policy = "Deny"
subnet_ids = [module.vpc.subnets] subnet_ids = [module.vpc.subnets]
whitelist = var.admin_user_whitelist whitelist = var.admin_user_whitelist
workspace_id = module.logs.workspace_id
} }

View File

@ -34,6 +34,7 @@ variable "networks" {
public = "10.1.1.0/24,public" # LBs public = "10.1.1.0/24,public" # LBs
private = "10.1.2.0/24,private" # k8s, postgres, keyvault private = "10.1.2.0/24,private" # k8s, postgres, keyvault
redis = "10.1.3.0/24,private" # Redis redis = "10.1.3.0/24,private" # Redis
apps = "10.1.4.0/24,private" # Redis
} }
} }
@ -43,23 +44,18 @@ variable "service_endpoints" {
public = "Microsoft.ContainerRegistry" # Not necessary but added to avoid infinite state loop public = "Microsoft.ContainerRegistry" # Not necessary but added to avoid infinite state loop
private = "Microsoft.Storage,Microsoft.KeyVault,Microsoft.ContainerRegistry,Microsoft.Sql" private = "Microsoft.Storage,Microsoft.KeyVault,Microsoft.ContainerRegistry,Microsoft.Sql"
redis = "Microsoft.Storage,Microsoft.Sql" # FIXME: There is no Microsoft.Redis redis = "Microsoft.Storage,Microsoft.Sql" # FIXME: There is no Microsoft.Redis
apps = "Microsoft.Storage,Microsoft.KeyVault,Microsoft.ContainerRegistry,Microsoft.Sql"
} }
} }
variable "gateway_subnet" {
type = string
default = "10.1.20.0/24"
}
variable "route_tables" { variable "route_tables" {
description = "Route tables and their default routes" description = "Route tables and their default routes"
type = map type = map
default = { default = {
public = "Internet" public = "Internet"
private = "Internet" private = "Internet" # TODO: Switch to FW
redis = "VnetLocal" redis = "VnetLocal"
#private = "VnetLocal" apps = "Internet" # TODO: Switch to FW
} }
} }

View File

@ -4,12 +4,9 @@ module "vpc" {
region = var.region region = var.region
virtual_network = var.virtual_network virtual_network = var.virtual_network
networks = var.networks networks = var.networks
gateway_subnet = var.gateway_subnet
route_tables = var.route_tables route_tables = var.route_tables
owner = var.owner owner = var.owner
name = var.name name = var.name
dns_servers = var.dns_servers dns_servers = var.dns_servers
service_endpoints = var.service_endpoints service_endpoints = var.service_endpoints
vpn_client_cidr = var.vpn_client_cidr
} }

View File

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

View File

@ -1,15 +1,18 @@
import pytest
import json import json
from uuid import uuid4
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from uuid import uuid4
import pytest
from tests.factories import ApplicationFactory, EnvironmentFactory from tests.factories import ApplicationFactory, EnvironmentFactory
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
from atst.domain.csp.cloud import AzureCloudProvider from atst.domain.csp.cloud import AzureCloudProvider
from atst.domain.csp.cloud.models import ( from atst.domain.csp.cloud.models import (
AdminRoleDefinitionCSPPayload,
AdminRoleDefinitionCSPResult,
ApplicationCSPPayload, ApplicationCSPPayload,
ApplicationCSPResult, ApplicationCSPResult,
BaseCSPPayload,
BillingInstructionCSPPayload, BillingInstructionCSPPayload,
BillingInstructionCSPResult, BillingInstructionCSPResult,
BillingProfileCreationCSPPayload, BillingProfileCreationCSPPayload,
@ -26,15 +29,20 @@ from atst.domain.csp.cloud.models import (
TaskOrderBillingCreationCSPResult, TaskOrderBillingCreationCSPResult,
TaskOrderBillingVerificationCSPPayload, TaskOrderBillingVerificationCSPPayload,
TaskOrderBillingVerificationCSPResult, TaskOrderBillingVerificationCSPResult,
TenantAdminOwnershipCSPPayload,
TenantAdminOwnershipCSPResult,
TenantCSPPayload, TenantCSPPayload,
TenantCSPResult, TenantCSPResult,
TenantPrincipalAppCSPPayload,
TenantPrincipalAppCSPResult,
TenantPrincipalCredentialCSPPayload,
TenantPrincipalCredentialCSPResult,
TenantPrincipalCSPPayload,
TenantPrincipalCSPResult,
TenantPrincipalOwnershipCSPPayload,
TenantPrincipalOwnershipCSPResult,
) )
creds = {
"home_tenant_id": "tenant_id",
"client_id": "client_id",
"secret_key": "secret_key",
}
BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31" BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31"
@ -98,8 +106,10 @@ MOCK_CREDS = {
} }
def mock_get_secret(azure, func): def mock_get_secret(azure, val=None):
azure.get_secret = func if val is None:
val = json.dumps(MOCK_CREDS)
azure.get_secret = lambda *a, **k: val
return azure return azure
@ -107,12 +117,12 @@ def mock_get_secret(azure, func):
def test_create_application_succeeds(mock_azure: AzureCloudProvider): def test_create_application_succeeds(mock_azure: AzureCloudProvider):
application = ApplicationFactory.create() application = ApplicationFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"}) mock_management_group_create(mock_azure, {"id": "Test Id"})
mock_azure = mock_get_secret(mock_azure)
mock_azure = mock_get_secret(mock_azure, lambda *a, **k: json.dumps(MOCK_CREDS))
payload = ApplicationCSPPayload( payload = ApplicationCSPPayload(
tenant_id="1234", display_name=application.name, parent_id=str(uuid4()) tenant_id="1234", display_name=application.name, parent_id=str(uuid4())
) )
result = mock_azure.create_application(payload) result = mock_azure.create_application(payload)
assert result.id == "Test Id" assert result.id == "Test Id"
@ -158,10 +168,6 @@ def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
def test_create_tenant(mock_azure: AzureCloudProvider): def test_create_tenant(mock_azure: AzureCloudProvider):
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
"accessToken": "TOKEN"
}
mock_result = Mock() mock_result = Mock()
mock_result.json.return_value = { mock_result.json.return_value = {
"objectId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d", "objectId": "0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
@ -172,7 +178,6 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
mock_azure.sdk.requests.post.return_value = mock_result mock_azure.sdk.requests.post.return_value = mock_result
payload = TenantCSPPayload( payload = TenantCSPPayload(
**dict( **dict(
creds=creds,
user_id="admin", user_id="admin",
password="JediJan13$coot", # pragma: allowlist secret password="JediJan13$coot", # pragma: allowlist secret
domain_name="jediccpospawnedtenant2", domain_name="jediccpospawnedtenant2",
@ -182,6 +187,7 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
password_recovery_email_address="thomas@promptworks.com", password_recovery_email_address="thomas@promptworks.com",
) )
) )
mock_azure = mock_get_secret(mock_azure)
result = mock_azure.create_tenant(payload) result = mock_azure.create_tenant(payload)
body: TenantCSPResult = result.get("body") body: TenantCSPResult = result.get("body")
assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435" assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
@ -209,7 +215,6 @@ def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
country="US", country="US",
postal_code="19109", postal_code="19109",
), ),
creds=creds,
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
billing_profile_display_name="Test Billing Profile", billing_profile_display_name="Test Billing Profile",
billing_account_name=BILLING_ACCOUNT_NAME, billing_account_name=BILLING_ACCOUNT_NAME,
@ -260,7 +265,7 @@ def test_validate_billing_profile_creation(mock_azure: AzureCloudProvider):
payload = BillingProfileVerificationCSPPayload( payload = BillingProfileVerificationCSPPayload(
**dict( **dict(
creds=creds, tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
billing_profile_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview", billing_profile_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview",
) )
) )
@ -299,7 +304,6 @@ def test_create_billing_profile_tenant_access(mock_azure: AzureCloudProvider):
payload = BillingProfileTenantAccessCSPPayload( payload = BillingProfileTenantAccessCSPPayload(
**dict( **dict(
creds=creds,
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435", tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
user_object_id="0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d", user_object_id="0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31", billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31",
@ -331,7 +335,7 @@ def test_create_task_order_billing_creation(mock_azure: AzureCloudProvider):
payload = TaskOrderBillingCreationCSPPayload( payload = TaskOrderBillingCreationCSPPayload(
**dict( **dict(
creds=creds, tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31", billing_account_name="7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31",
billing_profile_name="KQWI-W2SU-BG7-TGB", billing_profile_name="KQWI-W2SU-BG7-TGB",
) )
@ -392,7 +396,7 @@ def test_create_task_order_billing_verification(mock_azure):
payload = TaskOrderBillingVerificationCSPPayload( payload = TaskOrderBillingVerificationCSPPayload(
**dict( **dict(
creds=creds, tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
task_order_billing_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview", task_order_billing_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview",
) )
) )
@ -427,7 +431,7 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider):
payload = BillingInstructionCSPPayload( payload = BillingInstructionCSPPayload(
**dict( **dict(
creds=creds, tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
initial_clin_amount=1000.00, initial_clin_amount=1000.00,
initial_clin_start_date="2020/1/1", initial_clin_start_date="2020/1/1",
initial_clin_end_date="2020/3/1", initial_clin_end_date="2020/3/1",
@ -441,7 +445,6 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider):
body: BillingInstructionCSPResult = result.get("body") body: BillingInstructionCSPResult = result.get("body")
assert body.reported_clin_name == "TO1:CLIN001" assert body.reported_clin_name == "TO1:CLIN001"
def test_create_product_purchase(mock_azure: AzureCloudProvider): def test_create_product_purchase(mock_azure: AzureCloudProvider):
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = { mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
"accessToken": "TOKEN" "accessToken": "TOKEN"
@ -458,7 +461,7 @@ def test_create_product_purchase(mock_azure: AzureCloudProvider):
payload = ProductPurchaseCSPPayload( payload = ProductPurchaseCSPPayload(
**dict( **dict(
creds=creds, tenant_id="6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
type="AADPremium", type="AADPremium",
sku="AADP1", sku="AADP1",
productProperties={ productProperties={
@ -519,7 +522,7 @@ def test_create_product_purchase_verification(mock_azure):
payload = ProductPurchaseVerificationCSPPayload( payload = ProductPurchaseVerificationCSPPayload(
**dict( **dict(
creds=creds, tenant_id="6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
product_purchase_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview", product_purchase_verify_url="https://management.azure.com/providers/Microsoft.Billing/billingAccounts/7c89b735-b22b-55c0-ab5a-c624843e8bf6:de4416ce-acc6-44b1-8122-c87c4e903c91_2019-05-31/operationResults/createBillingProfile_478d5706-71f9-4a8b-8d4e-2cbaca27a668?api-version=2019-10-01-preview",
) )
) )
@ -527,3 +530,172 @@ def test_create_product_purchase_verification(mock_azure):
result = mock_azure.create_product_purchase_verification(payload) result = mock_azure.create_product_purchase_verification(payload)
body: ProductPurchaseVerificationCSPResult = result.get("body") body: ProductPurchaseVerificationCSPResult = result.get("body")
assert body.premium_purchase_date == "2020-01-30T18:57:05.981Z" assert body.premium_purchase_date == "2020-01-30T18:57:05.981Z"
def test_create_tenant_principal_app(mock_azure: AzureCloudProvider):
with patch.object(
AzureCloudProvider,
"_get_elevated_management_token",
wraps=mock_azure._get_elevated_management_token,
) as get_elevated_management_token:
get_elevated_management_token.return_value = "my fake token"
mock_result = Mock()
mock_result.ok = True
mock_result.json.return_value = {"appId": "appId", "id": "id"}
mock_azure.sdk.requests.post.return_value = mock_result
mock_azure = mock_get_secret(mock_azure)
payload = TenantPrincipalAppCSPPayload(
**{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"}
)
result: TenantPrincipalAppCSPResult = mock_azure.create_tenant_principal_app(
payload
)
assert result.principal_app_id == "appId"
def test_create_tenant_principal(mock_azure: AzureCloudProvider):
with patch.object(
AzureCloudProvider,
"_get_elevated_management_token",
wraps=mock_azure._get_elevated_management_token,
) as get_elevated_management_token:
get_elevated_management_token.return_value = "my fake token"
mock_result = Mock()
mock_result.ok = True
mock_result.json.return_value = {"id": "principal_id"}
mock_azure.sdk.requests.post.return_value = mock_result
mock_azure = mock_get_secret(mock_azure)
payload = TenantPrincipalCSPPayload(
**{
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
"principal_app_id": "appId",
}
)
result: TenantPrincipalCSPResult = mock_azure.create_tenant_principal(payload)
assert result.principal_id == "principal_id"
def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider):
with patch.object(
AzureCloudProvider,
"_get_elevated_management_token",
wraps=mock_azure._get_elevated_management_token,
) as get_elevated_management_token:
get_elevated_management_token.return_value = "my fake token"
mock_result = Mock()
mock_result.ok = True
mock_result.json.return_value = {"secretText": "new secret key"}
mock_azure.sdk.requests.post.return_value = mock_result
mock_azure = mock_get_secret(mock_azure)
payload = TenantPrincipalCredentialCSPPayload(
**{
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
"principal_app_id": "appId",
"principal_app_object_id": "appObjId",
}
)
result: TenantPrincipalCredentialCSPResult = mock_azure.create_tenant_principal_credential(
payload
)
assert result.principal_creds_established == True
def test_create_admin_role_definition(mock_azure: AzureCloudProvider):
with patch.object(
AzureCloudProvider,
"_get_elevated_management_token",
wraps=mock_azure._get_elevated_management_token,
) as get_elevated_management_token:
get_elevated_management_token.return_value = "my fake token"
mock_result = Mock()
mock_result.ok = True
mock_result.json.return_value = {
"value": [
{"id": "wrongid", "displayName": "Wrong Role"},
{"id": "id", "displayName": "Company Administrator"},
]
}
mock_azure.sdk.requests.get.return_value = mock_result
mock_azure = mock_get_secret(mock_azure)
payload = AdminRoleDefinitionCSPPayload(
**{"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4"}
)
result: AdminRoleDefinitionCSPResult = mock_azure.create_admin_role_definition(
payload
)
assert result.admin_role_def_id == "id"
def test_create_tenant_admin_ownership(mock_azure: AzureCloudProvider):
with patch.object(
AzureCloudProvider,
"_get_elevated_management_token",
wraps=mock_azure._get_elevated_management_token,
) as get_elevated_management_token:
get_elevated_management_token.return_value = "my fake token"
mock_result = Mock()
mock_result.ok = True
mock_result.json.return_value = {"id": "id"}
mock_azure.sdk.requests.put.return_value = mock_result
payload = TenantAdminOwnershipCSPPayload(
**{
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
"user_object_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d",
}
)
result: TenantAdminOwnershipCSPResult = mock_azure.create_tenant_admin_ownership(
payload
)
assert result.admin_owner_assignment_id == "id"
def test_create_tenant_principal_ownership(mock_azure: AzureCloudProvider):
with patch.object(
AzureCloudProvider,
"_get_elevated_management_token",
wraps=mock_azure._get_elevated_management_token,
) as get_elevated_management_token:
get_elevated_management_token.return_value = "my fake token"
mock_result = Mock()
mock_result.ok = True
mock_result.json.return_value = {"id": "id"}
mock_azure.sdk.requests.put.return_value = mock_result
payload = TenantPrincipalOwnershipCSPPayload(
**{
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
"principal_id": "971efe4d-1e80-4e39-b3b9-4e5c63ad446d",
}
)
result: TenantPrincipalOwnershipCSPResult = mock_azure.create_tenant_principal_ownership(
payload
)
assert result.principal_owner_assignment_id == "id"

View File

@ -106,10 +106,15 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
FSMStates.BILLING_INSTRUCTION_CREATED, FSMStates.BILLING_INSTRUCTION_CREATED,
FSMStates.PRODUCT_PURCHASE_CREATED, FSMStates.PRODUCT_PURCHASE_CREATED,
FSMStates.PRODUCT_PURCHASE_VERIFICATION_CREATED, FSMStates.PRODUCT_PURCHASE_VERIFICATION_CREATED,
FSMStates.TENANT_PRINCIPAL_APP_CREATED,
FSMStates.TENANT_PRINCIPAL_CREATED,
FSMStates.TENANT_PRINCIPAL_CREDENTIAL_CREATED,
FSMStates.ADMIN_ROLE_DEFINITION_CREATED,
FSMStates.PRINCIPAL_ADMIN_ROLE_CREATED,
FSMStates.TENANT_ADMIN_OWNERSHIP_CREATED,
FSMStates.TENANT_PRINCIPAL_OWNERSHIP_CREATED,
] ]
# Should source all creds for portfolio? might be easier to manage than per-step specific ones
creds = {"username": "mock-cloud", "password": "shh"} # pragma: allowlist secret
if portfolio.csp_data is not None: if portfolio.csp_data is not None:
csp_data = portfolio.csp_data csp_data = portfolio.csp_data
else: else:
@ -152,7 +157,7 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
collected_data = dict( collected_data = dict(
list(csp_data.items()) + list(portfolio_data.items()) + list(config.items()) list(csp_data.items()) + list(portfolio_data.items()) + list(config.items())
) )
sm.trigger_next_transition(creds=creds, csp_data=collected_data) sm.trigger_next_transition(csp_data=collected_data)
assert sm.state == expected_state assert sm.state == expected_state
if portfolio.csp_data is not None: if portfolio.csp_data is not None:
csp_data = portfolio.csp_data csp_data = portfolio.csp_data

View File

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

View File

@ -15,7 +15,7 @@ def test_environment_access_with_env_role(client, user_session):
url_for("applications.access_environment", environment_id=environment.id) url_for("applications.access_environment", environment_id=environment.id)
) )
assert response.status_code == 302 assert response.status_code == 302
assert "csp-environment-access" in response.location assert "portal.azure.com" in response.location
def test_environment_access_with_no_role(client, user_session): def test_environment_access_with_no_role(client, user_session):

View File

@ -458,3 +458,61 @@ def test_task_order_form_shows_errors(client, user_session, task_order):
body = response.data.decode() body = response.data.decode()
assert "There were some errors" in body assert "There were some errors" in body
assert "Not a valid decimal" in body assert "Not a valid decimal" in body
def test_update_and_render_next_handles_previous_valid_data(
client, user_session, task_order
):
user_session(task_order.portfolio.owner)
form_data = {"number": "0000000000000"}
original_number = task_order.number
response = client.post(
url_for(
"task_orders.submit_form_step_two_add_number",
task_order_id=task_order.id,
previous=True,
),
data=form_data,
)
assert response.status_code == 302
assert task_order.number == "0000000000000"
assert task_order.number != original_number
def test_update_and_render_next_handles_previous_invalid_data(
client, user_session, task_order
):
clin_list = [
{
"jedi_clin_type": "JEDI_CLIN_1",
"number": "12312",
"start_date": "01/01/2020",
"end_date": "01/01/2021",
"obligated_amount": "5000",
"total_amount": "10000",
},
]
TaskOrders.create_clins(task_order.id, clin_list)
assert len(task_order.clins) == 2
user_session(task_order.portfolio.owner)
form_data = {
"clins-0-jedi_clin_type": "JEDI_CLIN_1",
"clins-0-number": "12312",
"clins-0-start_date": "01/01/2020",
"clins-0-end_date": "01/01/2021",
"clins-0-obligated_amount": "5000",
"clins-0-total_amount": "10000",
"clins-1-jedi_clin_type": "JEDI_CLIN_1",
"clins-1-number": "1212",
}
response = client.post(
url_for(
"task_orders.submit_form_step_three_add_clins",
task_order_id=task_order.id,
previous=True,
),
data=form_data,
)
assert len(task_order.clins) == 2

View File

@ -1,7 +1,36 @@
from tests.factories import UserFactory from tests.factories import UserFactory, PortfolioFactory
from atst.routes import match_url_pattern
def test_root_redirects_if_user_is_logged_in(client, user_session): def test_root_redirects_if_user_is_logged_in(client, user_session):
user_session(UserFactory.create()) user_session(UserFactory.create())
response = client.get("/", follow_redirects=False) response = client.get("/", follow_redirects=False)
assert "home" in response.location assert "home" in response.location
def test_match_url_pattern(client):
assert not match_url_pattern(None)
assert match_url_pattern("/home") == "/home"
portfolio = PortfolioFactory()
# matches a URL with an argument
assert (
match_url_pattern(f"/portfolios/{portfolio.id}") # /portfolios/<portfolio_id>
== f"/portfolios/{portfolio.id}"
)
# matches a url with a query string
assert (
match_url_pattern(f"/portfolios/{portfolio.id}?foo=bar")
== f"/portfolios/{portfolio.id}?foo=bar"
)
# matches a URL only with a valid method
assert not match_url_pattern(f"/portfolios/{portfolio.id}/edit")
assert (
match_url_pattern(f"/portfolios/{portfolio.id}/edit", method="POST")
== f"/portfolios/{portfolio.id}/edit"
)
# returns None for URL that doesn't match a view function
assert not match_url_pattern("/pwned")
assert not match_url_pattern("http://www.hackersite.com/pwned")

View File

@ -28,7 +28,7 @@ def test_user_can_update_profile(user_session, client):
def test_user_is_redirected_when_updating_profile(user_session, client): def test_user_is_redirected_when_updating_profile(user_session, client):
user = UserFactory.create() user = UserFactory.create()
user_session(user) user_session(user)
next_url = "/requests" next_url = "/home"
user_data = user.to_dictionary() user_data = user.to_dictionary()
user_data["date_latest_training"] = user_data["date_latest_training"].strftime( user_data["date_latest_training"] = user_data["date_latest_training"].strftime(

View File

@ -19,9 +19,7 @@ from atst.app import make_config, make_app
_NO_ACCESS_CHECK_REQUIRED = _NO_LOGIN_REQUIRED + [ _NO_ACCESS_CHECK_REQUIRED = _NO_LOGIN_REQUIRED + [
"applications.accept_invitation", # available to all users; access control is built into invitation logic "applications.accept_invitation", # available to all users; access control is built into invitation logic
"atst.catch_all", # available to all users "atst.catch_all", # available to all users
"atst.csp_environment_access", # internal redirect
"atst.home", # available to all users "atst.home", # available to all users
"atst.jedi_csp_calculator", # internal redirect
"dev.messages", # dev tool "dev.messages", # dev tool
"dev.test_email", # dev tool "dev.test_email", # dev tool
"portfolios.accept_invitation", # available to all users; access control is built into invitation logic "portfolios.accept_invitation", # available to all users; access control is built into invitation logic

View File

@ -524,11 +524,16 @@ task_orders:
tooltip: tooltip:
obligated_funds: Funds committed to fund your portfolio. This may represent 100% of your total Task Order value, or a portion of it. obligated_funds: Funds committed to fund your portfolio. This may represent 100% of your total Task Order value, or a portion of it.
total_value: All obligated and projected funds for the Task Orders Base and Option CLINs. total_value: All obligated and projected funds for the Task Orders Base and Option CLINs.
expended_funds: All funds spend from the Task Order so far. expended_funds: All funds spent from the Task Order so far.
form: form:
add_clin: Add Another CLIN add_clin: Add Another CLIN
add_to_header: Enter the Task Order number add_to_header: Enter the Task Order number
add_to_description: Please input your 13-digit Task Order number. This number may be listed under "Order Number" if your Contracting Officer used form 1149, or "Delivery Order/Call No." if form 1155 was used. Moving forward, this portion of funding will be referenced by the recorded Task Order number. add_to_description: Please input your 13-digit Task Order number. This number may be listed under "Order Number" if your Contracting Officer used form 1149, or "Delivery Order/Call No." if form 1155 was used. Moving forward, this portion of funding will be referenced by the recorded Task Order number.
builder_base:
cancel_modal: Do you want to save this draft?
delete_draft: No, delete it
save_draft: Yes, save for later
to_number: "<strong>Task Order Number:</strong> {number}"
clin_title: Enter Contract Line Items clin_title: Enter Contract Line Items
clin_description: "Refer to your task order to locate your Contract Line Item Numbers (CLINs)." clin_description: "Refer to your task order to locate your Contract Line Item Numbers (CLINs)."
clin_details: CLIN Details clin_details: CLIN Details
@ -551,12 +556,16 @@ task_orders:
step_1: step_1:
title: Upload your approved Task Order (TO) title: Upload your approved Task Order (TO)
description: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future. description: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future.
next_button: "Next: Add TO Number"
step_2:
next_button: "Next: Add Base CLIN"
step_3: step_3:
next_button: "Next: Review Task Order" next_button: "Next: Review Task Order"
percent_obligated: "% of Funds Obligated" percent_obligated: "% of Funds Obligated"
step_4: step_4:
documents: Documents documents: Documents
clins: CLIN Summary clins: CLIN Summary
next_button: "Next: Confirm"
step_5: step_5:
cta_text: Verify Your Information cta_text: Verify Your Information
description: Prior to submitting the Task Order, you must acknowledge, by marking the appropriate box below, that the uploaded Task Order is signed by an appropriate, duly warranted Contracting Officer who has the authority to execute the uploaded Task Order on your Agencys behalf and has authorized you to upload the Task Order in accordance with Agency policy and procedures. You must further acknowledge, by marking the appropriate box below, that all information entered herein matches that of the submitted Task Order. description: Prior to submitting the Task Order, you must acknowledge, by marking the appropriate box below, that the uploaded Task Order is signed by an appropriate, duly warranted Contracting Officer who has the authority to execute the uploaded Task Order on your Agencys behalf and has authorized you to upload the Task Order in accordance with Agency policy and procedures. You must further acknowledge, by marking the appropriate box below, that all information entered herein matches that of the submitted Task Order.