resolve merge conflict with staging
This commit is contained in:
commit
7e4340e7e4
@ -3,7 +3,7 @@
|
||||
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
||||
"lines": null
|
||||
},
|
||||
"generated_at": "2020-01-19T20:21:20Z",
|
||||
"generated_at": "2020-01-29T16:40:16Z",
|
||||
"plugins_used": [
|
||||
{
|
||||
"base64_limit": 4.5,
|
||||
@ -145,7 +145,7 @@
|
||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 649,
|
||||
"line_number": 647,
|
||||
"type": "Hex High Entropy String"
|
||||
}
|
||||
]
|
||||
|
@ -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_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_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.
|
||||
- `SQLALCHEMY_ECHO`: Boolean value specifying if SQLAlchemy should log queries to stdout.
|
||||
- `STATIC_URL`: URL specifying where static assets are hosted.
|
||||
|
@ -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 ###
|
@ -1,12 +1,16 @@
|
||||
import json
|
||||
import re
|
||||
from secrets import token_urlsafe
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
from uuid import uuid4
|
||||
|
||||
from atst.utils import sha256_hex
|
||||
|
||||
from .cloud_provider_interface import CloudProviderInterface
|
||||
from .exceptions import AuthenticationException
|
||||
from .models import (
|
||||
AdminRoleDefinitionCSPPayload,
|
||||
AdminRoleDefinitionCSPResult,
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
@ -23,26 +27,36 @@ from .models import (
|
||||
ProductPurchaseCSPResult,
|
||||
ProductPurchaseVerificationCSPPayload,
|
||||
ProductPurchaseVerificationCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
TaskOrderBillingVerificationCSPResult,
|
||||
TenantAdminOwnershipCSPPayload,
|
||||
TenantAdminOwnershipCSPResult,
|
||||
TenantCSPPayload,
|
||||
TenantCSPResult,
|
||||
TenantPrincipalAppCSPPayload,
|
||||
TenantPrincipalAppCSPResult,
|
||||
TenantPrincipalCredentialCSPPayload,
|
||||
TenantPrincipalCredentialCSPResult,
|
||||
TenantPrincipalCSPPayload,
|
||||
TenantPrincipalCSPResult,
|
||||
TenantPrincipalOwnershipCSPPayload,
|
||||
TenantPrincipalOwnershipCSPResult,
|
||||
)
|
||||
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(
|
||||
"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,
|
||||
)
|
||||
|
||||
# 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"
|
||||
AZURE_MANAGEMENT_API = "https://management.azure.com"
|
||||
|
||||
|
||||
class AzureSDKProvider(object):
|
||||
@ -54,8 +68,9 @@ class AzureSDKProvider(object):
|
||||
import azure.identity as identity
|
||||
from azure.keyvault import secrets
|
||||
from azure.core import exceptions
|
||||
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
from msrestazure.azure_cloud import (
|
||||
AZURE_PUBLIC_CLOUD,
|
||||
) # TODO: choose cloud type from config
|
||||
import adal
|
||||
import requests
|
||||
|
||||
@ -70,7 +85,6 @@ class AzureSDKProvider(object):
|
||||
self.exceptions = exceptions
|
||||
self.secrets = secrets
|
||||
self.requests = requests
|
||||
# may change to a JEDI cloud
|
||||
self.cloud = AZURE_PUBLIC_CLOUD
|
||||
|
||||
|
||||
@ -82,6 +96,9 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
self.secret_key = config["AZURE_SECRET_KEY"]
|
||||
self.tenant_id = config["AZURE_TENANT_ID"]
|
||||
self.vault_url = config["AZURE_VAULT_URL"]
|
||||
self.ps_client_id = config["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:
|
||||
self.sdk = AzureSDKProvider()
|
||||
@ -91,7 +108,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
|
||||
|
||||
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(
|
||||
vault_url=self.vault_url, credential=credential,
|
||||
)
|
||||
@ -104,7 +121,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
)
|
||||
|
||||
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(
|
||||
vault_url=self.vault_url, credential=credential,
|
||||
)
|
||||
@ -180,7 +197,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"secret_key": creds.root_sp_key,
|
||||
"tenant_id": creds.root_tenant_id,
|
||||
},
|
||||
resource=AZURE_MANAGEMENT_API,
|
||||
resource=self.sdk.cloud.endpoints.resource_manager,
|
||||
)
|
||||
|
||||
response = self._create_management_group(
|
||||
@ -305,7 +322,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
)
|
||||
|
||||
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:
|
||||
raise AuthenticationException("Could not resolve token for tenant creation")
|
||||
|
||||
@ -317,26 +334,33 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
}
|
||||
|
||||
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,
|
||||
headers=create_tenant_headers,
|
||||
)
|
||||
|
||||
if result.status_code == 200:
|
||||
return self._ok(
|
||||
TenantCSPResult(
|
||||
**result.json(),
|
||||
tenant_admin_password=payload.password,
|
||||
tenant_admin_username=payload.user_id,
|
||||
)
|
||||
result_dict = result.json()
|
||||
tenant_id = result_dict.get("tenantId")
|
||||
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,
|
||||
),
|
||||
)
|
||||
return self._ok(TenantCSPResult(**result_dict))
|
||||
else:
|
||||
return self._error(result.json())
|
||||
|
||||
def create_billing_profile_creation(
|
||||
self, payload: BillingProfileCreationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for billing profile creation"
|
||||
@ -348,7 +372,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"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(
|
||||
billing_account_create_url,
|
||||
@ -368,7 +392,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_billing_profile_verification(
|
||||
self, payload: BillingProfileVerificationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for billing profile validation"
|
||||
@ -393,7 +417,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_billing_profile_tenant_access(
|
||||
self, payload: BillingProfileTenantAccessCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
request_body = {
|
||||
"properties": {
|
||||
"principalTenantId": payload.tenant_id, # from tenant creation
|
||||
@ -406,7 +430,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"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)
|
||||
if result.status_code == 201:
|
||||
@ -417,12 +441,12 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_task_order_billing_creation(
|
||||
self, payload: TaskOrderBillingCreationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
request_body = [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/enabledAzurePlans",
|
||||
"value": [{"skuId": "0001"}],
|
||||
"value": [{"skuId": AZURE_SKU_ID}],
|
||||
}
|
||||
]
|
||||
|
||||
@ -430,7 +454,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"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(
|
||||
url, headers=request_headers, json=request_body
|
||||
@ -447,7 +471,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_task_order_billing_verification(
|
||||
self, payload: TaskOrderBillingVerificationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for task order billing validation"
|
||||
@ -470,7 +494,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
return self._error(result.json())
|
||||
|
||||
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:
|
||||
raise AuthenticationException(
|
||||
"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 = {
|
||||
"Authorization": f"Bearer {sp_token}",
|
||||
@ -498,7 +522,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
return self._error(result.json())
|
||||
|
||||
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:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for aad premium product purchase"
|
||||
@ -540,7 +564,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_product_purchase_verification(
|
||||
self, payload: ProductPurchaseVerificationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for aad premium product purchase validation"
|
||||
@ -567,21 +591,198 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
else:
|
||||
return self._error(result.json())
|
||||
|
||||
def create_remote_admin(self, creds, tenant_details):
|
||||
# create app/service principal within tenant, with name constructed from tenant details
|
||||
# assign principal global admin
|
||||
def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload):
|
||||
mgmt_token = self._get_elevated_management_token(payload.tenant_id)
|
||||
|
||||
# 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
|
||||
response = {"clientId": "string", "secretKey": "string", "tenantId": "string"}
|
||||
return self._ok(
|
||||
{
|
||||
"client_id": response["clientId"],
|
||||
"secret_key": response["secret_key"],
|
||||
"tenant_id": response["tenantId"],
|
||||
request_body = {
|
||||
"properties": {
|
||||
"roleDefinitionId": role_definition_id,
|
||||
"principalId": payload.user_object_id,
|
||||
}
|
||||
}
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {mgmt_token}",
|
||||
}
|
||||
|
||||
assignment_guid = str(uuid4())
|
||||
|
||||
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01"
|
||||
|
||||
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):
|
||||
# use creds to update to force password recovery?
|
||||
@ -651,22 +852,42 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
if sub_id_match:
|
||||
return sub_id_match.group(1)
|
||||
|
||||
def _get_sp_token(self, creds):
|
||||
home_tenant_id = creds.get("home_tenant_id")
|
||||
client_id = creds.get("client_id")
|
||||
secret_key = creds.get("secret_key")
|
||||
def _get_tenant_admin_token(self, tenant_id, resource):
|
||||
creds = self._source_tenant_creds(tenant_id)
|
||||
return self._get_up_token_for_resource(
|
||||
creds.tenant_admin_username,
|
||||
creds.tenant_admin_password,
|
||||
tenant_id,
|
||||
resource,
|
||||
)
|
||||
|
||||
# TODO: Make endpoints consts or configs
|
||||
authentication_endpoint = "https://login.microsoftonline.com/"
|
||||
resource = "https://management.azure.com/"
|
||||
def _get_root_provisioning_token(self):
|
||||
creds = self._source_creds()
|
||||
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(
|
||||
authentication_endpoint + home_tenant_id
|
||||
f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
|
||||
)
|
||||
|
||||
# TODO: handle failure states here
|
||||
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)
|
||||
@ -680,16 +901,14 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
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(
|
||||
tenant_id=creds.get("tenant_id"),
|
||||
client_id=creds.get("client_id"),
|
||||
client_secret=creds.get("secret_key"),
|
||||
tenant_id=creds.tenant_id,
|
||||
client_id=creds.root_sp_client_id,
|
||||
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):
|
||||
return self._make_response("ok", body)
|
||||
|
||||
@ -716,6 +935,26 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"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:
|
||||
if 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"),
|
||||
)
|
||||
|
||||
def update_tenant_creds(self, tenant_id, secret):
|
||||
def update_tenant_creds(self, tenant_id, secret: KeyVaultCredentials):
|
||||
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):
|
||||
def _source_tenant_creds(self, tenant_id) -> KeyVaultCredentials:
|
||||
hashed = sha256_hex(tenant_id)
|
||||
raw_creds = self.get_secret(hashed)
|
||||
return KeyVaultCredentials(**json.loads(raw_creds))
|
||||
|
@ -1,41 +1,52 @@
|
||||
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 .exceptions import (
|
||||
AuthenticationException,
|
||||
AuthorizationException,
|
||||
BaselineProvisionException,
|
||||
ConnectionException,
|
||||
EnvironmentCreationException,
|
||||
GeneralCSPException,
|
||||
UnknownServerException,
|
||||
UserProvisioningException,
|
||||
UserRemovalException,
|
||||
)
|
||||
from .models import (
|
||||
AZURE_MGMNT_PATH,
|
||||
AdminRoleDefinitionCSPPayload,
|
||||
AdminRoleDefinitionCSPResult,
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
BillingInstructionCSPResult,
|
||||
BillingProfileCreationCSPPayload,
|
||||
BillingProfileCreationCSPResult,
|
||||
BillingProfileTenantAccessCSPResult,
|
||||
BillingProfileVerificationCSPPayload,
|
||||
BillingProfileVerificationCSPResult,
|
||||
ProductPurchaseCSPPayload,
|
||||
ProductPurchaseCSPResult,
|
||||
ProductPurchaseVerificationCSPPayload,
|
||||
ProductPurchaseVerificationCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
TaskOrderBillingVerificationCSPResult,
|
||||
TenantAdminOwnershipCSPPayload,
|
||||
TenantAdminOwnershipCSPResult,
|
||||
TenantCSPPayload,
|
||||
TenantCSPResult,
|
||||
TenantPrincipalAppCSPPayload,
|
||||
TenantPrincipalAppCSPResult,
|
||||
TenantPrincipalCredentialCSPPayload,
|
||||
TenantPrincipalCredentialCSPResult,
|
||||
TenantPrincipalCSPPayload,
|
||||
TenantPrincipalCSPResult,
|
||||
TenantPrincipalOwnershipCSPPayload,
|
||||
TenantPrincipalOwnershipCSPResult,
|
||||
)
|
||||
|
||||
|
||||
@ -124,7 +135,7 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
payload is an instance of TenantCSPPayload data class
|
||||
"""
|
||||
|
||||
self._authorize(payload.creds)
|
||||
self._authorize("admin")
|
||||
|
||||
self._delay(1, 5)
|
||||
|
||||
@ -304,6 +315,70 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
**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):
|
||||
self._authorize(auth_credentials)
|
||||
|
||||
|
@ -22,20 +22,10 @@ class AliasModel(BaseModel):
|
||||
|
||||
|
||||
class BaseCSPPayload(AliasModel):
|
||||
# {"username": "mock-cloud", "pass": "shh"}
|
||||
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)
|
||||
tenant_id: str
|
||||
|
||||
|
||||
class TenantCSPPayload(BaseCSPPayload):
|
||||
class TenantCSPPayload(AliasModel):
|
||||
user_id: str
|
||||
password: Optional[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/"
|
||||
|
||||
MANAGEMENT_GROUP_NAME_REGEX = "^[a-zA-Z0-9\-_\(\)\.]+$"
|
||||
|
@ -19,6 +19,13 @@ class AzureStages(Enum):
|
||||
BILLING_INSTRUCTION = "billing instruction"
|
||||
PRODUCT_PURCHASE = "purchase aad premium product"
|
||||
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):
|
||||
|
@ -168,14 +168,6 @@ class PortfolioStateMachine(
|
||||
self.portfolio.csp_data.update(response.dict())
|
||||
db.session.add(self.portfolio)
|
||||
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:
|
||||
app.logger.error(
|
||||
f"Failed to cast response to valid result class {self.__repr__()}:",
|
||||
|
@ -14,7 +14,9 @@ from flask import (
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
import pendulum
|
||||
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.authnid import AuthenticationContext
|
||||
@ -61,17 +63,36 @@ def _make_authentication_context():
|
||||
|
||||
|
||||
def redirect_after_login_url():
|
||||
if request.args.get("next"):
|
||||
returl = request.args.get("next")
|
||||
if request.args.get(app.form_cache.PARAM_NAME):
|
||||
returl += "?" + url.urlencode(
|
||||
{app.form_cache.PARAM_NAME: request.args.get(app.form_cache.PARAM_NAME)}
|
||||
)
|
||||
returl = request.args.get("next")
|
||||
if match_url_pattern(returl):
|
||||
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
|
||||
else:
|
||||
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):
|
||||
session["user_id"] = user.id
|
||||
session["last_login"] = user.last_login
|
||||
@ -109,13 +130,3 @@ def logout():
|
||||
@bp.route("/about")
|
||||
def about():
|
||||
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())
|
||||
|
@ -22,9 +22,4 @@ def wrap_environment_role_lookup(user, environment_id=None, **kwargs):
|
||||
@applications_bp.route("/environments/<environment_id>/access")
|
||||
@user_can(None, override=wrap_environment_role_lookup, message="access environment")
|
||||
def access_environment(environment_id):
|
||||
env_role = EnvironmentRoles.get_by_user_and_environment(
|
||||
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))
|
||||
return redirect("https://portal.azure.com")
|
||||
|
@ -70,7 +70,12 @@ def update_task_order(form, portfolio_id=None, task_order_id=None, flash_invalid
|
||||
|
||||
|
||||
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
|
||||
if task_order_id:
|
||||
@ -80,8 +85,9 @@ def update_and_render_next(
|
||||
form = TaskOrderForm(form_data)
|
||||
|
||||
task_order = update_task_order(form, portfolio_id, task_order_id)
|
||||
if task_order:
|
||||
return redirect(url_for(next_page, task_order_id=task_order.id))
|
||||
if task_order or previous:
|
||||
to_id = task_order.id if task_order else task_order_id
|
||||
return redirect(url_for(next_page, task_order_id=to_id))
|
||||
else:
|
||||
return (
|
||||
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"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
|
||||
def submit_form_step_two_add_number(task_order_id):
|
||||
previous = http_request.args.get("previous", "False").lower() == "true"
|
||||
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"
|
||||
|
||||
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"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
|
||||
def submit_form_step_three_add_clins(task_order_id):
|
||||
previous = http_request.args.get("previous", "False").lower() == "true"
|
||||
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"
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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.domain.users import Users
|
||||
from atst.utils.flash import formatted_flash as flash
|
||||
from atst.routes import match_url_pattern
|
||||
|
||||
|
||||
bp = Blueprint("users", __name__)
|
||||
@ -35,7 +36,7 @@ def update_user():
|
||||
if form.validate():
|
||||
Users.update(user, form.data)
|
||||
flash("user_updated")
|
||||
if next_url:
|
||||
if match_url_pattern(next_url):
|
||||
return redirect(next_url)
|
||||
|
||||
return render_template(
|
||||
|
@ -43,6 +43,7 @@ SERVER_NAME
|
||||
SESSION_COOKIE_NAME=atat
|
||||
SESSION_COOKIE_DOMAIN
|
||||
SESSION_KEY_PREFIX=session:
|
||||
SESSION_COOKIE_SECURE=false
|
||||
SESSION_TYPE = redis
|
||||
SESSION_USE_SIGNER = True
|
||||
SQLALCHEMY_ECHO = False
|
||||
|
@ -32,6 +32,7 @@ data:
|
||||
REDIS_HOST: atat.redis.cache.windows.net:6380
|
||||
REDIS_TLS: "true"
|
||||
SESSION_COOKIE_DOMAIN: atat.code.mil
|
||||
SESSION_COOKIE_SECURE: "true"
|
||||
STATIC_URL: https://atat-cdn.azureedge.net/static/
|
||||
TZ: UTC
|
||||
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini
|
||||
|
@ -10,7 +10,7 @@ data:
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
|
||||
# Set SSL protocols, ciphers, and related options
|
||||
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_ecdh_curve X25519:prime256v1:secp384r1;
|
||||
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 |
@ -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>
|
@ -14,10 +14,14 @@
|
||||
|
||||
{% call Modal(name='cancel', dismissable=True) %}
|
||||
<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">
|
||||
<button formaction="{{ cancel_discard_url }}" class="usa-button usa-button-primary" type="submit">No, delete it</button>
|
||||
<button formaction="{{ cancel_save_url }}" class="usa-button usa-button-primary" type="submit">Yes, save for later</button>
|
||||
<button formaction="{{ cancel_discard_url }}" class="usa-button usa-button-primary" type="submit">
|
||||
{{ "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>
|
||||
{% endcall %}
|
||||
@ -39,9 +43,20 @@
|
||||
{% endblock %}
|
||||
|
||||
{% if step != "1" %}
|
||||
<a class="usa-button usa-button-secondary" href="{{ previous_button_link }}">
|
||||
Previous
|
||||
</a>
|
||||
{% if step == "2" or step == "3" -%}
|
||||
<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>
|
||||
{%- endif %}
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
|
@ -7,7 +7,7 @@
|
||||
{%- endif %}
|
||||
{% if to_number %}
|
||||
<p>
|
||||
<strong>Task Order Number:</strong> {{ to_number }}
|
||||
{{ "task_orders.form.builder_base.to_number" | translate({ "number": to_number }) | safe }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if description %}
|
||||
|
@ -10,7 +10,7 @@
|
||||
{% set action = url_for("task_orders.submit_form_step_one_add_pdf", portfolio_id=portfolio.id) %}
|
||||
{% 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 sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %}
|
||||
|
||||
|
@ -4,8 +4,8 @@
|
||||
{% 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 next_button_text = "Next: Add Base CLIN" %}
|
||||
{% set previous_button_link = url_for("task_orders.form_step_one_add_pdf", task_order_id=task_order_id) %}
|
||||
{% set next_button_text = "task_orders.form.step_2.next_button" | translate %}
|
||||
{% 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 sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
{% 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 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 sticky_cta_text = 'task_orders.form.sticky_header_text' | translate %}
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
<a
|
||||
href="{{ action }}"
|
||||
class="usa-button usa-button-primary">
|
||||
Next: Confirm
|
||||
{{ "task_orders.form.step_4.next_button" | translate }}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -30,3 +30,11 @@ resource "azurerm_storage_container" "bucket" {
|
||||
storage_account_name = azurerm_storage_account.bucket.name
|
||||
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"
|
||||
}
|
||||
}
|
@ -29,3 +29,15 @@ resource "azurerm_cdn_endpoint" "cdn" {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,3 +29,7 @@ variable "origin_host_name" {
|
||||
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
|
||||
}
|
@ -36,8 +36,32 @@ resource "azurerm_container_registry" "acr" {
|
||||
virtual_network = [
|
||||
for subnet in var.subnet_ids : {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "workspace_id" {
|
||||
description = "The Log Analytics Workspace ID"
|
||||
type = string
|
||||
}
|
@ -39,3 +39,45 @@ resource "azurerm_kubernetes_cluster" "k8s" {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,3 +62,8 @@ variable "client_secret" {
|
||||
type = string
|
||||
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
|
||||
}
|
@ -76,4 +76,26 @@ resource "azurerm_key_vault_access_policy" "keyvault_admin_policy" {
|
||||
"backup",
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,4 +48,10 @@ variable "whitelist" {
|
||||
type = map
|
||||
description = "A map of whitelisted IPs and CIDR ranges. For single IPs, Azure expects just the IP, NOT a /32."
|
||||
default = {}
|
||||
}
|
||||
}
|
||||
|
||||
variable "workspace_id" {
|
||||
description = "Log Analytics Workspace ID for sending logs generated by this resource"
|
||||
type = string
|
||||
|
||||
}
|
||||
|
15
terraform/modules/log_analytics/main.tf
Normal file
15
terraform/modules/log_analytics/main.tf
Normal 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
|
||||
}
|
||||
}
|
3
terraform/modules/log_analytics/outputs.tf
Normal file
3
terraform/modules/log_analytics/outputs.tf
Normal file
@ -0,0 +1,3 @@
|
||||
output "workspace_id" {
|
||||
value = azurerm_log_analytics_workspace.log_workspace.id
|
||||
}
|
19
terraform/modules/log_analytics/variables.tf
Normal file
19
terraform/modules/log_analytics/variables.tf
Normal 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"
|
||||
}
|
@ -35,3 +35,33 @@ resource "azurerm_postgresql_virtual_network_rule" "sql" {
|
||||
subnet_id = var.subnet_id
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,3 +93,8 @@ variable "ssl_enforcement" {
|
||||
description = "Enforce SSL (Enabled/Disable)"
|
||||
default = "Enabled"
|
||||
}
|
||||
|
||||
variable "workspace_id" {
|
||||
description = "Log Analytics workspace for this resource to log to"
|
||||
type = string
|
||||
}
|
||||
|
@ -23,3 +23,16 @@ resource "azurerm_redis_cache" "redis" {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,3 +58,8 @@ variable "subnet_id" {
|
||||
type = string
|
||||
description = "Subnet ID that the service_endpoint should reside"
|
||||
}
|
||||
|
||||
variable "workspace_id" {
|
||||
description = "Log Analytics workspace for this resource to log to"
|
||||
type = string
|
||||
}
|
@ -72,45 +72,3 @@ resource "azurerm_route" "route" {
|
||||
address_prefix = "0.0.0.0/0"
|
||||
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"]
|
||||
}
|
||||
}
|
@ -34,7 +34,6 @@ variable "networks" {
|
||||
variable "dns_servers" {
|
||||
description = "DNS Server IPs for internal and public DNS lookups (must be on a defined subnet)"
|
||||
type = list
|
||||
|
||||
}
|
||||
|
||||
variable "route_tables" {
|
||||
@ -42,19 +41,8 @@ variable "route_tables" {
|
||||
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" {
|
||||
type = map
|
||||
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"]
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ module "cdn" {
|
||||
environment = var.environment
|
||||
name = var.name
|
||||
region = var.region
|
||||
workspace_id = module.logs.workspace_id
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ module "container_registry" {
|
||||
owner = var.owner
|
||||
backup_region = var.backup_region
|
||||
policy = "Deny"
|
||||
subnet_ids = []
|
||||
subnet_ids = [module.vpc.subnet_list["private"].id]
|
||||
whitelist = var.admin_user_whitelist
|
||||
workspace_id = module.logs.workspace_id
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ module "k8s" {
|
||||
min_count = 3
|
||||
client_id = data.azurerm_key_vault_secret.k8s_client_id.value
|
||||
client_secret = data.azurerm_key_vault_secret.k8s_client_secret.value
|
||||
workspace_id = module.logs.workspace_id
|
||||
}
|
||||
|
||||
#module "main_lb" {
|
||||
|
@ -10,5 +10,6 @@ module "keyvault" {
|
||||
policy = "Deny"
|
||||
subnet_ids = [module.vpc.subnets]
|
||||
whitelist = var.admin_user_whitelist
|
||||
workspace_id = module.logs.workspace_id
|
||||
}
|
||||
|
||||
|
8
terraform/providers/dev/logs.tf
Normal file
8
terraform/providers/dev/logs.tf
Normal file
@ -0,0 +1,8 @@
|
||||
module "logs" {
|
||||
source = "../../modules/log_analytics"
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
region = var.region
|
||||
name = var.name
|
||||
}
|
||||
|
@ -14,7 +14,8 @@ module "sql" {
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
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_password = data.azurerm_key_vault_secret.postgres_password.value
|
||||
workspace_id = module.logs.workspace_id
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
module "redis" {
|
||||
source = "../../modules/redis"
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
region = var.region
|
||||
name = var.name
|
||||
subnet_id = module.vpc.subnet_list["redis"].id
|
||||
sku_name = "Premium"
|
||||
family = "P"
|
||||
source = "../../modules/redis"
|
||||
owner = var.owner
|
||||
environment = var.environment
|
||||
region = var.region
|
||||
name = var.name
|
||||
subnet_id = module.vpc.subnet_list["redis"].id
|
||||
sku_name = "Premium"
|
||||
family = "P"
|
||||
workspace_id = module.logs.workspace_id
|
||||
}
|
||||
|
@ -10,4 +10,5 @@ module "operator_keyvault" {
|
||||
policy = "Deny"
|
||||
subnet_ids = [module.vpc.subnets]
|
||||
whitelist = var.admin_user_whitelist
|
||||
workspace_id = module.logs.workspace_id
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ variable "networks" {
|
||||
public = "10.1.1.0/24,public" # LBs
|
||||
private = "10.1.2.0/24,private" # k8s, postgres, keyvault
|
||||
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
|
||||
private = "Microsoft.Storage,Microsoft.KeyVault,Microsoft.ContainerRegistry,Microsoft.Sql"
|
||||
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" {
|
||||
description = "Route tables and their default routes"
|
||||
type = map
|
||||
default = {
|
||||
public = "Internet"
|
||||
private = "Internet"
|
||||
private = "Internet" # TODO: Switch to FW
|
||||
redis = "VnetLocal"
|
||||
#private = "VnetLocal"
|
||||
apps = "Internet" # TODO: Switch to FW
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,9 @@ module "vpc" {
|
||||
region = var.region
|
||||
virtual_network = var.virtual_network
|
||||
networks = var.networks
|
||||
gateway_subnet = var.gateway_subnet
|
||||
route_tables = var.route_tables
|
||||
owner = var.owner
|
||||
name = var.name
|
||||
dns_servers = var.dns_servers
|
||||
service_endpoints = var.service_endpoints
|
||||
vpn_client_cidr = var.vpn_client_cidr
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ Ex.
|
||||
```
|
||||
{
|
||||
'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 -->
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1,15 +1,18 @@
|
||||
import pytest
|
||||
import json
|
||||
from uuid import uuid4
|
||||
from unittest.mock import Mock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from tests.factories import ApplicationFactory, EnvironmentFactory
|
||||
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
|
||||
|
||||
from atst.domain.csp.cloud import AzureCloudProvider
|
||||
from atst.domain.csp.cloud.models import (
|
||||
AdminRoleDefinitionCSPPayload,
|
||||
AdminRoleDefinitionCSPResult,
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BaseCSPPayload,
|
||||
BillingInstructionCSPPayload,
|
||||
BillingInstructionCSPResult,
|
||||
BillingProfileCreationCSPPayload,
|
||||
@ -26,15 +29,20 @@ from atst.domain.csp.cloud.models import (
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
TaskOrderBillingVerificationCSPResult,
|
||||
TenantAdminOwnershipCSPPayload,
|
||||
TenantAdminOwnershipCSPResult,
|
||||
TenantCSPPayload,
|
||||
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"
|
||||
|
||||
|
||||
@ -98,8 +106,10 @@ MOCK_CREDS = {
|
||||
}
|
||||
|
||||
|
||||
def mock_get_secret(azure, func):
|
||||
azure.get_secret = func
|
||||
def mock_get_secret(azure, val=None):
|
||||
if val is None:
|
||||
val = json.dumps(MOCK_CREDS)
|
||||
azure.get_secret = lambda *a, **k: val
|
||||
|
||||
return azure
|
||||
|
||||
@ -107,12 +117,12 @@ def mock_get_secret(azure, func):
|
||||
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
||||
application = ApplicationFactory.create()
|
||||
mock_management_group_create(mock_azure, {"id": "Test Id"})
|
||||
|
||||
mock_azure = mock_get_secret(mock_azure, lambda *a, **k: json.dumps(MOCK_CREDS))
|
||||
mock_azure = mock_get_secret(mock_azure)
|
||||
|
||||
payload = ApplicationCSPPayload(
|
||||
tenant_id="1234", display_name=application.name, parent_id=str(uuid4())
|
||||
)
|
||||
|
||||
result = mock_azure.create_application(payload)
|
||||
|
||||
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):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
}
|
||||
|
||||
mock_result = Mock()
|
||||
mock_result.json.return_value = {
|
||||
"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
|
||||
payload = TenantCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
user_id="admin",
|
||||
password="JediJan13$coot", # pragma: allowlist secret
|
||||
domain_name="jediccpospawnedtenant2",
|
||||
@ -182,6 +187,7 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
|
||||
password_recovery_email_address="thomas@promptworks.com",
|
||||
)
|
||||
)
|
||||
mock_azure = mock_get_secret(mock_azure)
|
||||
result = mock_azure.create_tenant(payload)
|
||||
body: TenantCSPResult = result.get("body")
|
||||
assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
|
||||
@ -209,7 +215,6 @@ def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
|
||||
country="US",
|
||||
postal_code="19109",
|
||||
),
|
||||
creds=creds,
|
||||
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
|
||||
billing_profile_display_name="Test Billing Profile",
|
||||
billing_account_name=BILLING_ACCOUNT_NAME,
|
||||
@ -260,7 +265,7 @@ def test_validate_billing_profile_creation(mock_azure: AzureCloudProvider):
|
||||
|
||||
payload = BillingProfileVerificationCSPPayload(
|
||||
**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",
|
||||
)
|
||||
)
|
||||
@ -299,7 +304,6 @@ def test_create_billing_profile_tenant_access(mock_azure: AzureCloudProvider):
|
||||
|
||||
payload = BillingProfileTenantAccessCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
|
||||
user_object_id="0a5f4926-e3ee-4f47-a6e3-8b0a30a40e3d",
|
||||
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(
|
||||
**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_profile_name="KQWI-W2SU-BG7-TGB",
|
||||
)
|
||||
@ -392,7 +396,7 @@ def test_create_task_order_billing_verification(mock_azure):
|
||||
|
||||
payload = TaskOrderBillingVerificationCSPPayload(
|
||||
**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",
|
||||
)
|
||||
)
|
||||
@ -427,7 +431,7 @@ def test_create_billing_instruction(mock_azure: AzureCloudProvider):
|
||||
|
||||
payload = BillingInstructionCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
tenant_id="60ff9d34-82bf-4f21-b565-308ef0533435",
|
||||
initial_clin_amount=1000.00,
|
||||
initial_clin_start_date="2020/1/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")
|
||||
assert body.reported_clin_name == "TO1:CLIN001"
|
||||
|
||||
|
||||
def test_create_product_purchase(mock_azure: AzureCloudProvider):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
@ -458,7 +461,7 @@ def test_create_product_purchase(mock_azure: AzureCloudProvider):
|
||||
|
||||
payload = ProductPurchaseCSPPayload(
|
||||
**dict(
|
||||
creds=creds,
|
||||
tenant_id="6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
|
||||
type="AADPremium",
|
||||
sku="AADP1",
|
||||
productProperties={
|
||||
@ -519,7 +522,7 @@ def test_create_product_purchase_verification(mock_azure):
|
||||
|
||||
payload = ProductPurchaseVerificationCSPPayload(
|
||||
**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",
|
||||
)
|
||||
)
|
||||
@ -527,3 +530,172 @@ def test_create_product_purchase_verification(mock_azure):
|
||||
result = mock_azure.create_product_purchase_verification(payload)
|
||||
body: ProductPurchaseVerificationCSPResult = result.get("body")
|
||||
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"
|
||||
|
@ -106,10 +106,15 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
|
||||
FSMStates.BILLING_INSTRUCTION_CREATED,
|
||||
FSMStates.PRODUCT_PURCHASE_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:
|
||||
csp_data = portfolio.csp_data
|
||||
else:
|
||||
@ -152,7 +157,7 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
|
||||
collected_data = dict(
|
||||
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
|
||||
if portfolio.csp_data is not None:
|
||||
csp_data = portfolio.csp_data
|
||||
|
@ -9,6 +9,9 @@ AZURE_CONFIG = {
|
||||
"AZURE_TENANT_ID": "MOCK",
|
||||
"AZURE_POLICY_LOCATION": "policies",
|
||||
"AZURE_VAULT_URL": "http://vault",
|
||||
"POWERSHELL_CLIENT_ID": "MOCK",
|
||||
"AZURE_OWNER_ROLE_DEF_ID": "MOCK",
|
||||
"AZURE_GRAPH_RESOURCE": "MOCK",
|
||||
}
|
||||
|
||||
AUTH_CREDENTIALS = {
|
||||
@ -48,6 +51,12 @@ def mock_credentials():
|
||||
return Mock(spec=credentials)
|
||||
|
||||
|
||||
def mock_identity():
|
||||
import azure.identity as identity
|
||||
|
||||
return Mock(spec=identity)
|
||||
|
||||
|
||||
def mock_policy():
|
||||
from azure.mgmt.resource import policy
|
||||
|
||||
@ -72,15 +81,14 @@ def mock_secrets():
|
||||
return Mock(spec=secrets)
|
||||
|
||||
|
||||
def mock_identity():
|
||||
import azure.identity as identity
|
||||
def mock_cloud_details():
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
|
||||
return Mock(spec=identity)
|
||||
return AZURE_PUBLIC_CLOUD
|
||||
|
||||
|
||||
class MockAzureSDK(object):
|
||||
def __init__(self):
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
|
||||
self.subscription = mock_subscription()
|
||||
self.authorization = mock_authorization()
|
||||
@ -89,11 +97,11 @@ class MockAzureSDK(object):
|
||||
self.managementgroups = mock_managementgroups()
|
||||
self.graphrbac = mock_graphrbac()
|
||||
self.credentials = mock_credentials()
|
||||
self.identity = mock_identity()
|
||||
self.policy = mock_policy()
|
||||
self.secrets = mock_secrets()
|
||||
self.requests = mock_requests()
|
||||
# may change to a JEDI cloud
|
||||
self.cloud = AZURE_PUBLIC_CLOUD
|
||||
self.cloud = mock_cloud_details()
|
||||
self.identity = mock_identity()
|
||||
|
||||
|
||||
|
@ -15,7 +15,7 @@ def test_environment_access_with_env_role(client, user_session):
|
||||
url_for("applications.access_environment", environment_id=environment.id)
|
||||
)
|
||||
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):
|
||||
|
@ -458,3 +458,61 @@ def test_task_order_form_shows_errors(client, user_session, task_order):
|
||||
body = response.data.decode()
|
||||
assert "There were some errors" 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
|
||||
|
@ -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):
|
||||
user_session(UserFactory.create())
|
||||
response = client.get("/", follow_redirects=False)
|
||||
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")
|
||||
|
@ -28,7 +28,7 @@ def test_user_can_update_profile(user_session, client):
|
||||
def test_user_is_redirected_when_updating_profile(user_session, client):
|
||||
user = UserFactory.create()
|
||||
user_session(user)
|
||||
next_url = "/requests"
|
||||
next_url = "/home"
|
||||
|
||||
user_data = user.to_dictionary()
|
||||
user_data["date_latest_training"] = user_data["date_latest_training"].strftime(
|
||||
|
@ -19,9 +19,7 @@ from atst.app import make_config, make_app
|
||||
_NO_ACCESS_CHECK_REQUIRED = _NO_LOGIN_REQUIRED + [
|
||||
"applications.accept_invitation", # available to all users; access control is built into invitation logic
|
||||
"atst.catch_all", # available to all users
|
||||
"atst.csp_environment_access", # internal redirect
|
||||
"atst.home", # available to all users
|
||||
"atst.jedi_csp_calculator", # internal redirect
|
||||
"dev.messages", # dev tool
|
||||
"dev.test_email", # dev tool
|
||||
"portfolios.accept_invitation", # available to all users; access control is built into invitation logic
|
||||
|
@ -524,11 +524,16 @@ task_orders:
|
||||
tooltip:
|
||||
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 Order’s 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:
|
||||
add_clin: Add Another CLIN
|
||||
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.
|
||||
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_description: "Refer to your task order to locate your Contract Line Item Numbers (CLINs)."
|
||||
clin_details: CLIN Details
|
||||
@ -551,12 +556,16 @@ task_orders:
|
||||
step_1:
|
||||
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.
|
||||
next_button: "Next: Add TO Number"
|
||||
step_2:
|
||||
next_button: "Next: Add Base CLIN"
|
||||
step_3:
|
||||
next_button: "Next: Review Task Order"
|
||||
percent_obligated: "% of Funds Obligated"
|
||||
step_4:
|
||||
documents: Documents
|
||||
clins: CLIN Summary
|
||||
next_button: "Next: Confirm"
|
||||
step_5:
|
||||
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 Agency’s 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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user