Merge branch 'staging' into move-tf
This commit is contained in:
commit
98d932c4dc
@ -101,5 +101,7 @@ RUN mkdir /var/run/uwsgi && \
|
|||||||
chown -R atst:atat /var/run/uwsgi && \
|
chown -R atst:atat /var/run/uwsgi && \
|
||||||
chown -R atst:atat "${APP_DIR}"
|
chown -R atst:atat "${APP_DIR}"
|
||||||
|
|
||||||
|
RUN update-ca-certificates
|
||||||
|
|
||||||
# Run as the unprivileged APP user
|
# Run as the unprivileged APP user
|
||||||
USER atst
|
USER atst
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
"""Remove root_user_info from Environment
|
||||||
|
|
||||||
|
Revision ID: 0039308c6351
|
||||||
|
Revises: 17da2a475429
|
||||||
|
Create Date: 2020-02-04 14:37:06.814645
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0039308c6351' # pragma: allowlist secret
|
||||||
|
down_revision = '17da2a475429' # pragma: allowlist secret
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('environments', 'root_user_info')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('environments', sa.Column('root_user_info', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,29 @@
|
|||||||
|
"""add last_sent column to clins and pdf_last_sent to task_orders
|
||||||
|
|
||||||
|
Revision ID: 567bfb019a87
|
||||||
|
Revises: 0039308c6351
|
||||||
|
Create Date: 2020-01-31 14:06:21.926019
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '567bfb019a87' # pragma: allowlist secret
|
||||||
|
down_revision = '0039308c6351' # pragma: allowlist secret
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('clins', sa.Column('last_sent_at', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('pdf_last_sent_at', sa.DateTime(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('task_orders', 'pdf_last_sent_at')
|
||||||
|
op.drop_column('clins', 'last_sent_at')
|
||||||
|
# ### end Alembic commands ###
|
@ -6,12 +6,12 @@ from uuid import uuid4
|
|||||||
from atst.utils import sha256_hex
|
from atst.utils import sha256_hex
|
||||||
|
|
||||||
from .cloud_provider_interface import CloudProviderInterface
|
from .cloud_provider_interface import CloudProviderInterface
|
||||||
from .exceptions import AuthenticationException, UserProvisioningException
|
from .exceptions import (
|
||||||
|
AuthenticationException,
|
||||||
|
SecretException,
|
||||||
|
UserProvisioningException,
|
||||||
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
SubscriptionCreationCSPPayload,
|
|
||||||
SubscriptionCreationCSPResult,
|
|
||||||
SubscriptionVerificationCSPPayload,
|
|
||||||
SuscriptionVerificationCSPResult,
|
|
||||||
AdminRoleDefinitionCSPPayload,
|
AdminRoleDefinitionCSPPayload,
|
||||||
AdminRoleDefinitionCSPResult,
|
AdminRoleDefinitionCSPResult,
|
||||||
ApplicationCSPPayload,
|
ApplicationCSPPayload,
|
||||||
@ -24,14 +24,21 @@ from .models import (
|
|||||||
BillingProfileTenantAccessCSPResult,
|
BillingProfileTenantAccessCSPResult,
|
||||||
BillingProfileVerificationCSPPayload,
|
BillingProfileVerificationCSPPayload,
|
||||||
BillingProfileVerificationCSPResult,
|
BillingProfileVerificationCSPResult,
|
||||||
|
CostManagementQueryCSPResult,
|
||||||
|
EnvironmentCSPPayload,
|
||||||
|
EnvironmentCSPResult,
|
||||||
KeyVaultCredentials,
|
KeyVaultCredentials,
|
||||||
ManagementGroupCSPResponse,
|
PrincipalAdminRoleCSPPayload,
|
||||||
|
PrincipalAdminRoleCSPResult,
|
||||||
ProductPurchaseCSPPayload,
|
ProductPurchaseCSPPayload,
|
||||||
ProductPurchaseCSPResult,
|
ProductPurchaseCSPResult,
|
||||||
ProductPurchaseVerificationCSPPayload,
|
ProductPurchaseVerificationCSPPayload,
|
||||||
ProductPurchaseVerificationCSPResult,
|
ProductPurchaseVerificationCSPResult,
|
||||||
PrincipalAdminRoleCSPPayload,
|
ReportingCSPPayload,
|
||||||
PrincipalAdminRoleCSPResult,
|
SubscriptionCreationCSPPayload,
|
||||||
|
SubscriptionCreationCSPResult,
|
||||||
|
SubscriptionVerificationCSPPayload,
|
||||||
|
SuscriptionVerificationCSPResult,
|
||||||
TaskOrderBillingCreationCSPPayload,
|
TaskOrderBillingCreationCSPPayload,
|
||||||
TaskOrderBillingCreationCSPResult,
|
TaskOrderBillingCreationCSPResult,
|
||||||
TaskOrderBillingVerificationCSPPayload,
|
TaskOrderBillingVerificationCSPPayload,
|
||||||
@ -53,7 +60,6 @@ from .models import (
|
|||||||
)
|
)
|
||||||
from .policy import AzurePolicyManager
|
from .policy import AzurePolicyManager
|
||||||
|
|
||||||
|
|
||||||
# This needs to be a fully pathed role definition identifier, not just a UUID
|
# This needs to be a fully pathed role definition identifier, not just a UUID
|
||||||
# TODO: Extract these from sdk msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
# TODO: Extract these from sdk msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||||
AZURE_SKU_ID = "0001" # probably a static sku specific to ATAT/JEDI
|
AZURE_SKU_ID = "0001" # probably a static sku specific to ATAT/JEDI
|
||||||
@ -116,11 +122,15 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return secret_client.set_secret(secret_key, secret_value)
|
return secret_client.set_secret(secret_key, secret_value)
|
||||||
except self.exceptions.HttpResponseError:
|
except self.sdk.exceptions.HttpResponseError as exc:
|
||||||
app.logger.error(
|
app.logger.error(
|
||||||
f"Could not SET secret in Azure keyvault for key {secret_key}.",
|
f"Could not SET secret in Azure keyvault for key {secret_key}.",
|
||||||
exc_info=1,
|
exc_info=1,
|
||||||
)
|
)
|
||||||
|
raise SecretException(
|
||||||
|
f"Could not SET secret in Azure keyvault for key {secret_key}.",
|
||||||
|
exc.message,
|
||||||
|
)
|
||||||
|
|
||||||
def get_secret(self, secret_key):
|
def get_secret(self, secret_key):
|
||||||
credential = self._get_client_secret_credential_obj()
|
credential = self._get_client_secret_credential_obj()
|
||||||
@ -129,67 +139,35 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return secret_client.get_secret(secret_key).value
|
return secret_client.get_secret(secret_key).value
|
||||||
except self.exceptions.HttpResponseError:
|
except self.sdk.exceptions.HttpResponseError:
|
||||||
app.logger.error(
|
app.logger.error(
|
||||||
f"Could not GET secret in Azure keyvault for key {secret_key}.",
|
f"Could not GET secret in Azure keyvault for key {secret_key}.",
|
||||||
exc_info=1,
|
exc_info=1,
|
||||||
)
|
)
|
||||||
|
raise SecretException(
|
||||||
|
f"Could not GET secret in Azure keyvault for key {secret_key}.",
|
||||||
|
exc.message,
|
||||||
|
)
|
||||||
|
|
||||||
def create_environment(self, auth_credentials: Dict, user, environment):
|
def create_environment(self, payload: EnvironmentCSPPayload):
|
||||||
# since this operation would only occur within a tenant, should we source the tenant
|
creds = self._source_creds(payload.tenant_id)
|
||||||
# via lookup from environment once we've created the portfolio csp data schema
|
credentials = self._get_credential_obj(
|
||||||
# something like this:
|
{
|
||||||
# environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None)
|
"client_id": creds.tenant_sp_client_id,
|
||||||
# though we'd probably source the whole credentials for these calls from the portfolio csp
|
"secret_key": creds.tenant_sp_key,
|
||||||
# data, as it would have to be where we store the creds for the at-at user within the portfolio tenant
|
"tenant_id": creds.tenant_id,
|
||||||
# credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds())
|
},
|
||||||
credentials = self._get_credential_obj(self._root_creds)
|
resource=self.sdk.cloud.endpoints.resource_manager,
|
||||||
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
|
||||||
management_group_id = "?" # management group id chained from environment
|
|
||||||
parent_id = "?" # from environment.application
|
|
||||||
|
|
||||||
management_group = self._create_management_group(
|
|
||||||
credentials, management_group_id, display_name, parent_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return ManagementGroupCSPResponse(**management_group)
|
response = self._create_management_group(
|
||||||
|
|
||||||
def create_atat_admin_user(
|
|
||||||
self, auth_credentials: Dict, csp_environment_id: str
|
|
||||||
) -> Dict:
|
|
||||||
root_creds = self._root_creds
|
|
||||||
credentials = self._get_credential_obj(root_creds)
|
|
||||||
|
|
||||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
|
||||||
subscription = sub_client.subscriptions.get(csp_environment_id)
|
|
||||||
|
|
||||||
managment_principal = self._get_management_service_principal()
|
|
||||||
|
|
||||||
auth_client = self.sdk.authorization.AuthorizationManagementClient(
|
|
||||||
credentials,
|
credentials,
|
||||||
# TODO: Determine which subscription this needs to point at
|
payload.management_group_name,
|
||||||
# Once we're in a multi-sub environment
|
payload.display_name,
|
||||||
subscription.id,
|
payload.parent_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create role assignment for
|
return EnvironmentCSPResult(**response)
|
||||||
role_assignment_id = str(uuid4())
|
|
||||||
role_assignment_create_params = auth_client.role_assignments.models.RoleAssignmentCreateParameters(
|
|
||||||
role_definition_id=REMOTE_ROOT_ROLE_DEF_ID,
|
|
||||||
principal_id=managment_principal.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
auth_client.role_assignments.create(
|
|
||||||
scope=f"/subscriptions/{subscription.id}/",
|
|
||||||
role_assignment_name=role_assignment_id,
|
|
||||||
parameters=role_assignment_create_params,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"csp_user_id": managment_principal.object_id,
|
|
||||||
"credentials": managment_principal.password_credentials,
|
|
||||||
"role_name": role_assignment_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_application(self, payload: ApplicationCSPPayload):
|
def create_application(self, payload: ApplicationCSPPayload):
|
||||||
creds = self._source_creds(payload.tenant_id)
|
creds = self._source_creds(payload.tenant_id)
|
||||||
@ -798,17 +776,6 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
|
|
||||||
return self._ok()
|
return self._ok()
|
||||||
|
|
||||||
def create_billing_alerts(self, TBD):
|
|
||||||
# TODO: Add azure-mgmt-consumption for Budget and Notification entities/operations
|
|
||||||
# TODO: Determine how to auth against that API using the SDK, doesn't seeem possible at the moment
|
|
||||||
# TODO: billing alerts are registered as Notifications on Budget objects, which have start/end dates
|
|
||||||
# TODO: determine what the keys in the Notifications dict are supposed to be
|
|
||||||
# we may need to rotate budget objects when new TOs/CLINs are reported?
|
|
||||||
|
|
||||||
# we likely only want the budget ID, can be updated or replaced?
|
|
||||||
response = {"id": "id"}
|
|
||||||
return self._ok({"budget_id": response["id"]})
|
|
||||||
|
|
||||||
def _get_management_service_principal(self):
|
def _get_management_service_principal(self):
|
||||||
# we really should be using graph.microsoft.com, but i'm getting
|
# we really should be using graph.microsoft.com, but i'm getting
|
||||||
# "expired token" errors for that
|
# "expired token" errors for that
|
||||||
@ -1070,3 +1037,41 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
hashed = sha256_hex(tenant_id)
|
hashed = sha256_hex(tenant_id)
|
||||||
raw_creds = self.get_secret(hashed)
|
raw_creds = self.get_secret(hashed)
|
||||||
return KeyVaultCredentials(**json.loads(raw_creds))
|
return KeyVaultCredentials(**json.loads(raw_creds))
|
||||||
|
|
||||||
|
def get_reporting_data(self, payload: ReportingCSPPayload):
|
||||||
|
"""
|
||||||
|
Queries the Cost Management API for an invoice section's raw reporting data
|
||||||
|
|
||||||
|
We query at the invoiceSection scope. The full scope path is passed in
|
||||||
|
with the payload at the `invoice_section_id` key.
|
||||||
|
"""
|
||||||
|
creds = self._source_tenant_creds(payload.tenant_id)
|
||||||
|
token = self._get_sp_token(
|
||||||
|
payload.tenant_id, creds.tenant_sp_client_id, creds.tenant_sp_key
|
||||||
|
)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
raise AuthenticationException("Could not retrieve tenant access token")
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
request_body = {
|
||||||
|
"type": "Usage",
|
||||||
|
"timeframe": "Custom",
|
||||||
|
"timePeriod": {"from": payload.from_date, "to": payload.to_date,},
|
||||||
|
"dataset": {
|
||||||
|
"granularity": "Daily",
|
||||||
|
"aggregation": {"totalCost": {"name": "PreTaxCost", "function": "Sum"}},
|
||||||
|
"grouping": [{"type": "Dimension", "name": "InvoiceId"}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cost_mgmt_url = (
|
||||||
|
f"/providers/Microsoft.CostManagement/query?api-version=2019-11-01"
|
||||||
|
)
|
||||||
|
result = self.sdk.requests.post(
|
||||||
|
f"{self.sdk.cloud.endpoints.resource_manager}{payload.invoice_section_id}{cost_mgmt_url}",
|
||||||
|
json=request_body,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if result.ok:
|
||||||
|
return CostManagementQueryCSPResult(**result.json())
|
||||||
|
@ -11,7 +11,7 @@ class CloudProviderInterface:
|
|||||||
def root_creds(self) -> Dict:
|
def root_creds(self) -> Dict:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def create_environment(self, auth_credentials: Dict, user, environment) -> str:
|
def create_environment(self, payload):
|
||||||
"""Create a new environment in the CSP.
|
"""Create a new environment in the CSP.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@ -31,33 +31,6 @@ class CloudProviderInterface:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def create_atat_admin_user(
|
|
||||||
self, auth_credentials: Dict, csp_environment_id: str
|
|
||||||
) -> Dict:
|
|
||||||
"""Creates a new, programmatic user in the CSP. Grants this user full permissions to administer
|
|
||||||
the CSP.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
auth_credentials -- Object containing CSP account credentials
|
|
||||||
csp_environment_id -- ID of the CSP Environment the admin user should be created in
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
object: Object representing new remote admin user, including credentials
|
|
||||||
Something like:
|
|
||||||
{
|
|
||||||
"user_id": string,
|
|
||||||
"credentials": dict, # structure TBD based on csp
|
|
||||||
}
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthenticationException: Problem with the credentials
|
|
||||||
AuthorizationException: Credentials not authorized for current action(s)
|
|
||||||
ConnectionException: Issue with the CSP API connection
|
|
||||||
UnknownServerException: Unknown issue on the CSP side
|
|
||||||
UserProvisioningException: Problem creating the root user
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def create_or_update_user(
|
def create_or_update_user(
|
||||||
self, auth_credentials: Dict, user_info, csp_role_id: str
|
self, auth_credentials: Dict, user_info, csp_role_id: str
|
||||||
) -> str:
|
) -> str:
|
||||||
|
@ -118,3 +118,17 @@ class BaselineProvisionException(GeneralCSPException):
|
|||||||
return "Could not complete baseline provisioning for environment ({}): {}".format(
|
return "Could not complete baseline provisioning for environment ({}): {}".format(
|
||||||
self.env_identifier, self.reason
|
self.env_identifier, self.reason
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SecretException(GeneralCSPException):
|
||||||
|
"""A problem occurred with setting or getting secrets"""
|
||||||
|
|
||||||
|
def __init__(self, tenant_id, reason):
|
||||||
|
self.tenant_id = tenant_id
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
return "Could not get or set secret for ({}): {}".format(
|
||||||
|
self.tenant_id, self.reason
|
||||||
|
)
|
||||||
|
@ -4,9 +4,7 @@ from .cloud_provider_interface import CloudProviderInterface
|
|||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
AuthenticationException,
|
AuthenticationException,
|
||||||
AuthorizationException,
|
AuthorizationException,
|
||||||
BaselineProvisionException,
|
|
||||||
ConnectionException,
|
ConnectionException,
|
||||||
EnvironmentCreationException,
|
|
||||||
GeneralCSPException,
|
GeneralCSPException,
|
||||||
UnknownServerException,
|
UnknownServerException,
|
||||||
UserProvisioningException,
|
UserProvisioningException,
|
||||||
@ -25,16 +23,21 @@ from .models import (
|
|||||||
BillingProfileTenantAccessCSPResult,
|
BillingProfileTenantAccessCSPResult,
|
||||||
BillingProfileVerificationCSPPayload,
|
BillingProfileVerificationCSPPayload,
|
||||||
BillingProfileVerificationCSPResult,
|
BillingProfileVerificationCSPResult,
|
||||||
|
CostManagementQueryCSPResult,
|
||||||
|
CostManagementQueryProperties,
|
||||||
ProductPurchaseCSPPayload,
|
ProductPurchaseCSPPayload,
|
||||||
ProductPurchaseCSPResult,
|
ProductPurchaseCSPResult,
|
||||||
ProductPurchaseVerificationCSPPayload,
|
ProductPurchaseVerificationCSPPayload,
|
||||||
ProductPurchaseVerificationCSPResult,
|
ProductPurchaseVerificationCSPResult,
|
||||||
PrincipalAdminRoleCSPPayload,
|
PrincipalAdminRoleCSPPayload,
|
||||||
PrincipalAdminRoleCSPResult,
|
PrincipalAdminRoleCSPResult,
|
||||||
|
ReportingCSPPayload,
|
||||||
SubscriptionCreationCSPPayload,
|
SubscriptionCreationCSPPayload,
|
||||||
SubscriptionCreationCSPResult,
|
SubscriptionCreationCSPResult,
|
||||||
SubscriptionVerificationCSPPayload,
|
SubscriptionVerificationCSPPayload,
|
||||||
SuscriptionVerificationCSPResult,
|
SuscriptionVerificationCSPResult,
|
||||||
|
EnvironmentCSPPayload,
|
||||||
|
EnvironmentCSPResult,
|
||||||
TaskOrderBillingCreationCSPPayload,
|
TaskOrderBillingCreationCSPPayload,
|
||||||
TaskOrderBillingCreationCSPResult,
|
TaskOrderBillingCreationCSPResult,
|
||||||
TaskOrderBillingVerificationCSPPayload,
|
TaskOrderBillingVerificationCSPPayload,
|
||||||
@ -91,34 +94,6 @@ class MockCloudProvider(CloudProviderInterface):
|
|||||||
def get_secret(self, secret_key: str, default=dict()):
|
def get_secret(self, secret_key: str, default=dict()):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def create_environment(self, auth_credentials, user, environment):
|
|
||||||
self._authorize(auth_credentials)
|
|
||||||
|
|
||||||
self._delay(1, 5)
|
|
||||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
|
||||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
|
||||||
self._maybe_raise(
|
|
||||||
self.ENV_CREATE_FAILURE_PCT,
|
|
||||||
EnvironmentCreationException(
|
|
||||||
environment.id, "Could not create environment."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
csp_environment_id = self._id()
|
|
||||||
|
|
||||||
self._delay(1, 5)
|
|
||||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
|
||||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
|
||||||
self._maybe_raise(
|
|
||||||
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
|
||||||
BaselineProvisionException(
|
|
||||||
csp_environment_id, "Could not create environment baseline."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
|
||||||
|
|
||||||
return csp_environment_id
|
|
||||||
|
|
||||||
def create_subscription(self, payload: SubscriptionCreationCSPPayload):
|
def create_subscription(self, payload: SubscriptionCreationCSPPayload):
|
||||||
return self.create_subscription_creation(payload)
|
return self.create_subscription_creation(payload)
|
||||||
|
|
||||||
@ -142,23 +117,6 @@ class MockCloudProvider(CloudProviderInterface):
|
|||||||
subscription_id="subscriptions/60fbbb72-0516-4253-ab18-c92432ba3230"
|
subscription_id="subscriptions/60fbbb72-0516-4253-ab18-c92432ba3230"
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_atat_admin_user(self, auth_credentials, csp_environment_id):
|
|
||||||
self._authorize(auth_credentials)
|
|
||||||
|
|
||||||
self._delay(1, 5)
|
|
||||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
|
||||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
|
||||||
self._maybe_raise(
|
|
||||||
self.ATAT_ADMIN_CREATE_FAILURE_PCT,
|
|
||||||
UserProvisioningException(
|
|
||||||
csp_environment_id, "atat_admin", "Could not create admin user."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
|
||||||
|
|
||||||
return {"id": self._id(), "credentials": self._auth_credentials}
|
|
||||||
|
|
||||||
def create_tenant(self, payload: TenantCSPPayload):
|
def create_tenant(self, payload: TenantCSPPayload):
|
||||||
"""
|
"""
|
||||||
payload is an instance of TenantCSPPayload data class
|
payload is an instance of TenantCSPPayload data class
|
||||||
@ -477,6 +435,13 @@ class MockCloudProvider(CloudProviderInterface):
|
|||||||
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
|
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def create_environment(self, payload: EnvironmentCSPPayload):
|
||||||
|
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||||
|
|
||||||
|
return EnvironmentCSPResult(
|
||||||
|
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
|
||||||
|
)
|
||||||
|
|
||||||
def create_user(self, payload: UserCSPPayload):
|
def create_user(self, payload: UserCSPPayload):
|
||||||
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||||
|
|
||||||
@ -487,3 +452,25 @@ class MockCloudProvider(CloudProviderInterface):
|
|||||||
|
|
||||||
def update_tenant_creds(self, tenant_id, secret):
|
def update_tenant_creds(self, tenant_id, secret):
|
||||||
return secret
|
return secret
|
||||||
|
|
||||||
|
def get_reporting_data(self, payload: ReportingCSPPayload):
|
||||||
|
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||||
|
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||||
|
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||||
|
object_id = str(uuid4())
|
||||||
|
|
||||||
|
properties = CostManagementQueryProperties(
|
||||||
|
**dict(
|
||||||
|
columns=[
|
||||||
|
{"name": "PreTaxCost", "type": "Number"},
|
||||||
|
{"name": "UsageDate", "type": "Number"},
|
||||||
|
{"name": "InvoiceId", "type": "String"},
|
||||||
|
{"name": "Currency", "type": "String"},
|
||||||
|
],
|
||||||
|
rows=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return CostManagementQueryCSPResult(
|
||||||
|
**dict(name=object_id, properties=properties,)
|
||||||
|
)
|
||||||
|
@ -358,6 +358,14 @@ class ApplicationCSPResult(ManagementGroupCSPResponse):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentCSPPayload(ManagementGroupCSPPayload):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentCSPResult(ManagementGroupCSPResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class KeyVaultCredentials(BaseModel):
|
class KeyVaultCredentials(BaseModel):
|
||||||
root_sp_client_id: Optional[str]
|
root_sp_client_id: Optional[str]
|
||||||
root_sp_key: Optional[str]
|
root_sp_key: Optional[str]
|
||||||
@ -499,3 +507,34 @@ class UserCSPPayload(BaseCSPPayload):
|
|||||||
|
|
||||||
class UserCSPResult(AliasModel):
|
class UserCSPResult(AliasModel):
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
class QueryColumn(AliasModel):
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
|
||||||
|
|
||||||
|
class CostManagementQueryProperties(AliasModel):
|
||||||
|
columns: List[QueryColumn]
|
||||||
|
rows: List[Optional[list]]
|
||||||
|
|
||||||
|
|
||||||
|
class CostManagementQueryCSPResult(AliasModel):
|
||||||
|
name: str
|
||||||
|
properties: CostManagementQueryProperties
|
||||||
|
|
||||||
|
|
||||||
|
class ReportingCSPPayload(BaseCSPPayload):
|
||||||
|
invoice_section_id: str
|
||||||
|
from_date: str
|
||||||
|
to_date: str
|
||||||
|
|
||||||
|
@root_validator(pre=True)
|
||||||
|
def extract_invoice_section(cls, values):
|
||||||
|
try:
|
||||||
|
values["invoice_section_id"] = values["billing_profile_properties"][
|
||||||
|
"invoice_sections"
|
||||||
|
][0]["invoice_section_id"]
|
||||||
|
return values
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
raise ValueError("Invoice section ID not present in payload")
|
||||||
|
@ -43,10 +43,12 @@ class AzureFileService(FileService):
|
|||||||
|
|
||||||
from azure.storage.common import CloudStorageAccount
|
from azure.storage.common import CloudStorageAccount
|
||||||
from azure.storage.blob import BlobSasPermissions
|
from azure.storage.blob import BlobSasPermissions
|
||||||
|
from azure.storage.blob.models import BlobPermissions
|
||||||
from azure.storage.blob.blockblobservice import BlockBlobService
|
from azure.storage.blob.blockblobservice import BlockBlobService
|
||||||
|
|
||||||
self.CloudStorageAccount = CloudStorageAccount
|
self.CloudStorageAccount = CloudStorageAccount
|
||||||
self.BlobSasPermissions = BlobSasPermissions
|
self.BlobSasPermissions = BlobSasPermissions
|
||||||
|
self.BlobPermissions = BlobPermissions
|
||||||
self.BlockBlobService = BlockBlobService
|
self.BlockBlobService = BlockBlobService
|
||||||
|
|
||||||
def get_token(self):
|
def get_token(self):
|
||||||
@ -72,20 +74,22 @@ class AzureFileService(FileService):
|
|||||||
return ({"token": sas_token}, object_name)
|
return ({"token": sas_token}, object_name)
|
||||||
|
|
||||||
def generate_download_link(self, object_name, filename):
|
def generate_download_link(self, object_name, filename):
|
||||||
account = self.CloudStorageAccount(
|
block_blob_service = self.BlockBlobService(
|
||||||
account_name=self.account_name, account_key=self.storage_key
|
account_name=self.account_name, account_key=self.storage_key
|
||||||
)
|
)
|
||||||
bbs = account.create_block_blob_service()
|
sas_token = block_blob_service.generate_blob_shared_access_signature(
|
||||||
sas_token = bbs.generate_blob_shared_access_signature(
|
container_name=self.container_name,
|
||||||
self.container_name,
|
blob_name=object_name,
|
||||||
object_name,
|
permission=self.BlobPermissions(read=True),
|
||||||
permission=self.BlobSasPermissions(read=True),
|
|
||||||
expiry=datetime.utcnow() + self.timeout,
|
expiry=datetime.utcnow() + self.timeout,
|
||||||
content_disposition=f"attachment; filename={filename}",
|
content_disposition=f"attachment; filename={filename}",
|
||||||
protocol="https",
|
protocol="https",
|
||||||
)
|
)
|
||||||
return bbs.make_blob_url(
|
return block_blob_service.make_blob_url(
|
||||||
self.container_name, object_name, protocol="https", sas_token=sas_token
|
container_name=self.container_name,
|
||||||
|
blob_name=object_name,
|
||||||
|
protocol="https",
|
||||||
|
sas_token=sas_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
def download_task_order(self, object_name):
|
def download_task_order(self, object_name):
|
||||||
|
@ -106,9 +106,11 @@ class EnvironmentRoles(object):
|
|||||||
def disable(cls, environment_role_id):
|
def disable(cls, environment_role_id):
|
||||||
environment_role = EnvironmentRoles.get_by_id(environment_role_id)
|
environment_role = EnvironmentRoles.get_by_id(environment_role_id)
|
||||||
|
|
||||||
if environment_role.csp_user_id and not environment_role.environment.is_pending:
|
if environment_role.csp_user_id and not environment_role.environment.cloud_id:
|
||||||
credentials = environment_role.environment.csp_credentials
|
tenant_id = environment_role.environment.application.portfolio.csp_data.get(
|
||||||
app.csp.cloud.disable_user(credentials, environment_role.csp_user_id)
|
"tenant_id"
|
||||||
|
)
|
||||||
|
app.csp.cloud.disable_user(tenant_id, environment_role.csp_user_id)
|
||||||
|
|
||||||
environment_role.status = EnvironmentRole.Status.DISABLED
|
environment_role.status = EnvironmentRole.Status.DISABLED
|
||||||
db.session.add(environment_role)
|
db.session.add(environment_role)
|
||||||
|
@ -124,18 +124,9 @@ class Environments(object):
|
|||||||
Any environment with an active CLIN that doesn't yet have a `cloud_id`.
|
Any environment with an active CLIN that doesn't yet have a `cloud_id`.
|
||||||
"""
|
"""
|
||||||
results = (
|
results = (
|
||||||
cls.base_provision_query(now).filter(Environment.cloud_id == None).all()
|
cls.base_provision_query(now)
|
||||||
|
.filter(Application.cloud_id != None)
|
||||||
|
.filter(Environment.cloud_id.is_(None))
|
||||||
|
.all()
|
||||||
)
|
)
|
||||||
return [id_ for id_, in results]
|
return [id_ for id_, in results]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_environments_pending_atat_user_creation(cls, now) -> List[UUID]:
|
|
||||||
"""
|
|
||||||
Any environment with an active CLIN that has a cloud_id but no `root_user_info`.
|
|
||||||
"""
|
|
||||||
results = (
|
|
||||||
cls.base_provision_query(now)
|
|
||||||
.filter(Environment.cloud_id != None)
|
|
||||||
.filter(Environment.root_user_info == None)
|
|
||||||
).all()
|
|
||||||
return [id_ for id_, in results]
|
|
||||||
|
@ -15,6 +15,8 @@ from atst.models import (
|
|||||||
Permissions,
|
Permissions,
|
||||||
PortfolioRole,
|
PortfolioRole,
|
||||||
PortfolioRoleStatus,
|
PortfolioRoleStatus,
|
||||||
|
TaskOrder,
|
||||||
|
CLIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .query import PortfoliosQuery, PortfolioStateMachinesQuery
|
from .query import PortfoliosQuery, PortfolioStateMachinesQuery
|
||||||
@ -144,7 +146,7 @@ class Portfolios(object):
|
|||||||
return db.session.query(Portfolio.id)
|
return db.session.query(Portfolio.id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_portfolios_pending_provisioning(cls) -> List[UUID]:
|
def get_portfolios_pending_provisioning(cls, now) -> List[UUID]:
|
||||||
"""
|
"""
|
||||||
Any portfolio with a corresponding State Machine that is either:
|
Any portfolio with a corresponding State Machine that is either:
|
||||||
not started yet,
|
not started yet,
|
||||||
@ -153,22 +155,18 @@ class Portfolios(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
cls.base_provision_query()
|
db.session.query(Portfolio.id)
|
||||||
.join(PortfolioStateMachine)
|
.join(PortfolioStateMachine)
|
||||||
|
.join(TaskOrder)
|
||||||
|
.join(CLIN)
|
||||||
|
.filter(Portfolio.deleted == False)
|
||||||
|
.filter(CLIN.start_date <= now)
|
||||||
|
.filter(CLIN.end_date > now)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
PortfolioStateMachine.state == FSMStates.UNSTARTED,
|
PortfolioStateMachine.state == FSMStates.UNSTARTED,
|
||||||
PortfolioStateMachine.state == FSMStates.FAILED,
|
PortfolioStateMachine.state.like("%CREATED"),
|
||||||
PortfolioStateMachine.state == FSMStates.TENANT_FAILED,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return [id_ for id_, in results]
|
return [id_ for id_, in results]
|
||||||
|
|
||||||
# db.session.query(PortfolioStateMachine).\
|
|
||||||
# filter(
|
|
||||||
# or_(
|
|
||||||
# PortfolioStateMachine.state==FSMStates.UNSTARTED,
|
|
||||||
# PortfolioStateMachine.state==FSMStates.UNSTARTED,
|
|
||||||
# )
|
|
||||||
# ).all()
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import datetime
|
from datetime import datetime
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models.clin import CLIN
|
from atst.models.clin import CLIN
|
||||||
@ -40,7 +41,7 @@ class TaskOrders(BaseDomainClass):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def sign(cls, task_order, signer_dod_id):
|
def sign(cls, task_order, signer_dod_id):
|
||||||
task_order.signer_dod_id = signer_dod_id
|
task_order.signer_dod_id = signer_dod_id
|
||||||
task_order.signed_at = datetime.datetime.now()
|
task_order.signed_at = datetime.now()
|
||||||
|
|
||||||
db.session.add(task_order)
|
db.session.add(task_order)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -76,3 +77,17 @@ class TaskOrders(BaseDomainClass):
|
|||||||
task_order = TaskOrders.get(task_order_id)
|
task_order = TaskOrders.get(task_order_id)
|
||||||
db.session.delete(task_order)
|
db.session.delete(task_order)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_send_task_order_files(cls):
|
||||||
|
return (
|
||||||
|
db.session.query(TaskOrder)
|
||||||
|
.join(CLIN)
|
||||||
|
.filter(
|
||||||
|
or_(
|
||||||
|
TaskOrder.pdf_last_sent_at < CLIN.last_sent_at,
|
||||||
|
TaskOrder.pdf_last_sent_at.is_(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
104
atst/jobs.py
104
atst/jobs.py
@ -1,18 +1,25 @@
|
|||||||
from flask import current_app as app
|
|
||||||
import pendulum
|
import pendulum
|
||||||
|
from flask import current_app as app
|
||||||
|
from smtplib import SMTPException
|
||||||
|
from azure.core.exceptions import AzureError
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.queue import celery
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
from atst.models import JobFailure
|
|
||||||
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
|
||||||
from atst.domain.csp.cloud import CloudProviderInterface
|
|
||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
|
from atst.domain.csp.cloud import CloudProviderInterface
|
||||||
|
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||||
|
from atst.domain.csp.cloud.models import (
|
||||||
|
ApplicationCSPPayload,
|
||||||
|
EnvironmentCSPPayload,
|
||||||
|
UserCSPPayload,
|
||||||
|
)
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.domain.application_roles import ApplicationRoles
|
from atst.models import JobFailure
|
||||||
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.models.utils import claim_for_update, claim_many_for_update
|
from atst.models.utils import claim_for_update, claim_many_for_update
|
||||||
|
from atst.queue import celery
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
from atst.domain.csp.cloud.models import ApplicationCSPPayload, UserCSPPayload
|
|
||||||
|
|
||||||
|
|
||||||
class RecordFailure(celery.Task):
|
class RecordFailure(celery.Task):
|
||||||
@ -40,8 +47,8 @@ class RecordFailure(celery.Task):
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(ignore_result=True)
|
@celery.task(ignore_result=True)
|
||||||
def send_mail(recipients, subject, body):
|
def send_mail(recipients, subject, body, attachments=[]):
|
||||||
app.mailer.send(recipients, subject, body)
|
app.mailer.send(recipients, subject, body, attachments)
|
||||||
|
|
||||||
|
|
||||||
@celery.task(ignore_result=True)
|
@celery.task(ignore_result=True)
|
||||||
@ -109,45 +116,17 @@ def do_create_environment(csp: CloudProviderInterface, environment_id=None):
|
|||||||
with claim_for_update(environment) as environment:
|
with claim_for_update(environment) as environment:
|
||||||
|
|
||||||
if environment.cloud_id is not None:
|
if environment.cloud_id is not None:
|
||||||
# TODO: Return value for this?
|
|
||||||
return
|
return
|
||||||
|
|
||||||
user = environment.creator
|
csp_details = environment.application.portfolio.csp_data
|
||||||
|
parent_id = environment.application.cloud_id
|
||||||
# we'll need to do some checking in this job for cases where it's retrying
|
tenant_id = csp_details.get("tenant_id")
|
||||||
# when a failure occured after some successful steps
|
payload = EnvironmentCSPPayload(
|
||||||
# (e.g. if environment.cloud_id is not None, then we can skip first step)
|
tenant_id=tenant_id, display_name=environment.name, parent_id=parent_id
|
||||||
|
|
||||||
# credentials either from a given user or pulled from config?
|
|
||||||
# if using global creds, do we need to log what user authorized action?
|
|
||||||
atat_root_creds = csp.root_creds()
|
|
||||||
|
|
||||||
# user is needed because baseline root account in the environment will
|
|
||||||
# be assigned to the requesting user, open question how to handle duplicate
|
|
||||||
# email addresses across new environments
|
|
||||||
csp_environment_id = csp.create_environment(atat_root_creds, user, environment)
|
|
||||||
environment.cloud_id = csp_environment_id
|
|
||||||
db.session.add(environment)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
body = render_email(
|
|
||||||
"emails/application/environment_ready.txt", {"environment": environment}
|
|
||||||
)
|
|
||||||
app.mailer.send(
|
|
||||||
[environment.creator.email], translate("email.environment_ready"), body
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
env_result = csp.create_environment(payload)
|
||||||
def do_create_atat_admin_user(csp: CloudProviderInterface, environment_id=None):
|
environment.cloud_id = env_result.id
|
||||||
environment = Environments.get(environment_id)
|
|
||||||
|
|
||||||
with claim_for_update(environment) as environment:
|
|
||||||
atat_root_creds = csp.root_creds()
|
|
||||||
|
|
||||||
atat_remote_root_user = csp.create_atat_admin_user(
|
|
||||||
atat_root_creds, environment.cloud_id
|
|
||||||
)
|
|
||||||
environment.root_user_info = atat_remote_root_user
|
|
||||||
db.session.add(environment)
|
db.session.add(environment)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@ -191,19 +170,12 @@ def create_environment(self, environment_id=None):
|
|||||||
do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id)
|
do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id)
|
||||||
|
|
||||||
|
|
||||||
@celery.task(bind=True, base=RecordFailure)
|
|
||||||
def create_atat_admin_user(self, environment_id=None):
|
|
||||||
do_work(
|
|
||||||
do_create_atat_admin_user, self, app.csp.cloud, environment_id=environment_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@celery.task(bind=True)
|
@celery.task(bind=True)
|
||||||
def dispatch_provision_portfolio(self):
|
def dispatch_provision_portfolio(self):
|
||||||
"""
|
"""
|
||||||
Iterate over portfolios with a corresponding State Machine that have not completed.
|
Iterate over portfolios with a corresponding State Machine that have not completed.
|
||||||
"""
|
"""
|
||||||
for portfolio_id in Portfolios.get_portfolios_pending_provisioning():
|
for portfolio_id in Portfolios.get_portfolios_pending_provisioning(pendulum.now()):
|
||||||
provision_portfolio.delay(portfolio_id=portfolio_id)
|
provision_portfolio.delay(portfolio_id=portfolio_id)
|
||||||
|
|
||||||
|
|
||||||
@ -233,3 +205,31 @@ def dispatch_create_atat_admin_user(self):
|
|||||||
pendulum.now()
|
pendulum.now()
|
||||||
):
|
):
|
||||||
create_atat_admin_user.delay(environment_id=environment_id)
|
create_atat_admin_user.delay(environment_id=environment_id)
|
||||||
|
|
||||||
|
|
||||||
|
@celery.task(bind=True)
|
||||||
|
def dispatch_send_task_order_files(self):
|
||||||
|
task_orders = TaskOrders.get_for_send_task_order_files()
|
||||||
|
recipients = [app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")]
|
||||||
|
|
||||||
|
for task_order in task_orders:
|
||||||
|
subject = translate(
|
||||||
|
"email.task_order_sent.subject", {"to_number": task_order.number}
|
||||||
|
)
|
||||||
|
body = translate("email.task_order_sent.body", {"to_number": task_order.number})
|
||||||
|
|
||||||
|
try:
|
||||||
|
file = app.csp.files.download_task_order(task_order.pdf.object_name)
|
||||||
|
file["maintype"] = "application"
|
||||||
|
file["subtype"] = "pdf"
|
||||||
|
send_mail(
|
||||||
|
recipients=recipients, subject=subject, body=body, attachments=[file]
|
||||||
|
)
|
||||||
|
except (AzureError, SMTPException) as err:
|
||||||
|
app.logger.exception(err)
|
||||||
|
continue
|
||||||
|
|
||||||
|
task_order.pdf_last_sent_at = pendulum.now()
|
||||||
|
db.session.add(task_order)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from sqlalchemy import Column, Date, Enum as SQLAEnum, ForeignKey, Numeric, String
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
Date,
|
||||||
|
DateTime,
|
||||||
|
Enum as SQLAEnum,
|
||||||
|
ForeignKey,
|
||||||
|
Numeric,
|
||||||
|
String,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
@ -29,6 +37,7 @@ class CLIN(Base, mixins.TimestampsMixin):
|
|||||||
total_amount = Column(Numeric(scale=2), nullable=False)
|
total_amount = Column(Numeric(scale=2), nullable=False)
|
||||||
obligated_amount = Column(Numeric(scale=2), nullable=False)
|
obligated_amount = Column(Numeric(scale=2), nullable=False)
|
||||||
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False)
|
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False)
|
||||||
|
last_sent_at = Column(DateTime)
|
||||||
|
|
||||||
#
|
#
|
||||||
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3
|
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
from sqlalchemy import Column, ForeignKey, String, UniqueConstraint
|
from sqlalchemy import Column, ForeignKey, String, UniqueConstraint
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from atst.models.base import Base
|
|
||||||
import atst.models.mixins as mixins
|
import atst.models.mixins as mixins
|
||||||
import atst.models.types as types
|
import atst.models.types as types
|
||||||
|
from atst.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
class Environment(
|
class Environment(
|
||||||
@ -30,7 +28,6 @@ class Environment(
|
|||||||
creator = relationship("User")
|
creator = relationship("User")
|
||||||
|
|
||||||
cloud_id = Column(String)
|
cloud_id = Column(String)
|
||||||
root_user_info = Column(JSONB(none_as_null=True))
|
|
||||||
|
|
||||||
roles = relationship(
|
roles = relationship(
|
||||||
"EnvironmentRole",
|
"EnvironmentRole",
|
||||||
@ -44,10 +41,6 @@ class Environment(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class ProvisioningStatus(Enum):
|
|
||||||
PENDING = "pending"
|
|
||||||
COMPLETED = "completed"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users(self):
|
def users(self):
|
||||||
return {r.application_role.user for r in self.roles}
|
return {r.application_role.user for r in self.roles}
|
||||||
@ -68,16 +61,9 @@ class Environment(
|
|||||||
def portfolio_id(self):
|
def portfolio_id(self):
|
||||||
return self.application.portfolio_id
|
return self.application.portfolio_id
|
||||||
|
|
||||||
@property
|
|
||||||
def provisioning_status(self) -> ProvisioningStatus:
|
|
||||||
if self.cloud_id is None or self.root_user_info is None:
|
|
||||||
return self.ProvisioningStatus.PENDING
|
|
||||||
else:
|
|
||||||
return self.ProvisioningStatus.COMPLETED
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_pending(self):
|
def is_pending(self):
|
||||||
return self.provisioning_status == self.ProvisioningStatus.PENDING
|
return self.cloud_id is None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
||||||
@ -91,11 +77,3 @@ class Environment(
|
|||||||
@property
|
@property
|
||||||
def history(self):
|
def history(self):
|
||||||
return self.get_changes()
|
return self.get_changes()
|
||||||
|
|
||||||
@property
|
|
||||||
def csp_credentials(self):
|
|
||||||
return (
|
|
||||||
self.root_user_info.get("credentials")
|
|
||||||
if self.root_user_info is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
@ -175,11 +175,14 @@ class PortfolioStateMachine(
|
|||||||
app.logger.info(exc.json())
|
app.logger.info(exc.json())
|
||||||
print(exc.json())
|
print(exc.json())
|
||||||
app.logger.info(payload_data)
|
app.logger.info(payload_data)
|
||||||
|
# TODO: Ensure that failing the stage does not preclude a Celery retry
|
||||||
self.fail_stage(stage)
|
self.fail_stage(stage)
|
||||||
|
# TODO: catch and handle general CSP exception here
|
||||||
except (ConnectionException, UnknownServerException) as exc:
|
except (ConnectionException, UnknownServerException) as exc:
|
||||||
app.logger.error(
|
app.logger.error(
|
||||||
f"CSP api call. Caught exception for {self.__repr__()}.", exc_info=1,
|
f"CSP api call. Caught exception for {self.__repr__()}.", exc_info=1,
|
||||||
)
|
)
|
||||||
|
# TODO: Ensure that failing the stage does not preclude a Celery retry
|
||||||
self.fail_stage(stage)
|
self.fail_stage(stage)
|
||||||
|
|
||||||
self.finish_stage(stage)
|
self.finish_stage(stage)
|
||||||
|
@ -39,6 +39,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
|
|
||||||
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
||||||
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
||||||
|
pdf_last_sent_at = Column(DateTime)
|
||||||
number = Column(String, unique=True,) # Task Order Number
|
number = Column(String, unique=True,) # Task Order Number
|
||||||
signer_dod_id = Column(String)
|
signer_dod_id = Column(String)
|
||||||
signed_at = Column(DateTime)
|
signed_at = Column(DateTime)
|
||||||
|
@ -19,10 +19,6 @@ def update_celery(celery, app):
|
|||||||
"task": "atst.jobs.dispatch_create_environment",
|
"task": "atst.jobs.dispatch_create_environment",
|
||||||
"schedule": 60,
|
"schedule": 60,
|
||||||
},
|
},
|
||||||
"beat-dispatch_create_atat_admin_user": {
|
|
||||||
"task": "atst.jobs.dispatch_create_atat_admin_user",
|
|
||||||
"schedule": 60,
|
|
||||||
},
|
|
||||||
"beat-dispatch_create_user": {
|
"beat-dispatch_create_user": {
|
||||||
"task": "atst.jobs.dispatch_create_user",
|
"task": "atst.jobs.dispatch_create_user",
|
||||||
"schedule": 60,
|
"schedule": 60,
|
||||||
|
@ -467,9 +467,10 @@ def revoke_invite(application_id, application_role_id):
|
|||||||
if invite.is_pending:
|
if invite.is_pending:
|
||||||
ApplicationInvitations.revoke(invite.token)
|
ApplicationInvitations.revoke(invite.token)
|
||||||
flash(
|
flash(
|
||||||
"application_invite_revoked",
|
"invite_revoked",
|
||||||
|
resource="Application",
|
||||||
user_name=app_role.user_name,
|
user_name=app_role.user_name,
|
||||||
application_name=g.application.name,
|
resource_name=g.application.name,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
flash(
|
flash(
|
||||||
|
@ -37,8 +37,14 @@ def accept_invitation(portfolio_token):
|
|||||||
)
|
)
|
||||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="revoke invitation")
|
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="revoke invitation")
|
||||||
def revoke_invitation(portfolio_id, portfolio_token):
|
def revoke_invitation(portfolio_id, portfolio_token):
|
||||||
PortfolioInvitations.revoke(portfolio_token)
|
invite = PortfolioInvitations.revoke(portfolio_token)
|
||||||
|
|
||||||
|
flash(
|
||||||
|
"invite_revoked",
|
||||||
|
resource="Portfolio",
|
||||||
|
user_name=invite.user_name,
|
||||||
|
resource_name=g.portfolio.name,
|
||||||
|
)
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"portfolios.admin",
|
"portfolios.admin",
|
||||||
|
@ -19,6 +19,9 @@ from atst.models import (
|
|||||||
def get_resources_from_context(view_args):
|
def get_resources_from_context(view_args):
|
||||||
query = None
|
query = None
|
||||||
|
|
||||||
|
if view_args is None:
|
||||||
|
view_args = {}
|
||||||
|
|
||||||
if "portfolio_token" in view_args:
|
if "portfolio_token" in view_args:
|
||||||
query = (
|
query = (
|
||||||
db.session.query(Portfolio)
|
db.session.query(Portfolio)
|
||||||
|
@ -33,11 +33,6 @@ MESSAGES = {
|
|||||||
"message": "flash.application_invite.resent.message",
|
"message": "flash.application_invite.resent.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"application_invite_revoked": {
|
|
||||||
"title": "flash.application_invite.revoked.title",
|
|
||||||
"message": "flash.application_invite.revoked.message",
|
|
||||||
"category": "success",
|
|
||||||
},
|
|
||||||
"application_member_removed": {
|
"application_member_removed": {
|
||||||
"title": "flash.application_member.removed.title",
|
"title": "flash.application_member.removed.title",
|
||||||
"message": "flash.application_member.removed.message",
|
"message": "flash.application_member.removed.message",
|
||||||
@ -103,6 +98,11 @@ MESSAGES = {
|
|||||||
"message": None,
|
"message": None,
|
||||||
"category": "warning",
|
"category": "warning",
|
||||||
},
|
},
|
||||||
|
"invite_revoked": {
|
||||||
|
"title": "flash.invite_revoked.title",
|
||||||
|
"message": "flash.invite_revoked.message",
|
||||||
|
"category": "success",
|
||||||
|
},
|
||||||
"logged_out": {
|
"logged_out": {
|
||||||
"title": "flash.logged_out.title",
|
"title": "flash.logged_out.title",
|
||||||
"message": "flash.logged_out.message",
|
"message": "flash.logged_out.message",
|
||||||
|
@ -1,30 +1,21 @@
|
|||||||
|
import ExpandSidenavMixin from '../mixins/expand_sidenav'
|
||||||
import ToggleMixin from '../mixins/toggle'
|
import ToggleMixin from '../mixins/toggle'
|
||||||
|
|
||||||
const cookieName = 'expandSidenav'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'sidenav-toggler',
|
name: 'sidenav-toggler',
|
||||||
|
|
||||||
mixins: [ToggleMixin],
|
mixins: [ExpandSidenavMixin, ToggleMixin],
|
||||||
|
|
||||||
props: {
|
mounted: function() {
|
||||||
defaultVisible: {
|
this.$parent.$emit('sidenavToggle', this.isVisible)
|
||||||
type: Boolean,
|
|
||||||
default: function() {
|
|
||||||
if (document.cookie.match(cookieName)) {
|
|
||||||
return !!document.cookie.match(cookieName + ' *= *true')
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggle: function(e) {
|
toggle: function(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.isVisible = !this.isVisible
|
this.isVisible = !this.isVisible
|
||||||
document.cookie = cookieName + '=' + this.isVisible + '; path=/'
|
document.cookie = this.cookieName + '=' + this.isVisible + '; path=/'
|
||||||
|
this.$parent.$emit('sidenavToggle', this.isVisible)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export default {
|
|||||||
filename: {
|
filename: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
objectName: {
|
initialObjectName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
initialErrors: {
|
initialErrors: {
|
||||||
@ -42,6 +42,7 @@ export default {
|
|||||||
filenameError: false,
|
filenameError: false,
|
||||||
downloadLink: '',
|
downloadLink: '',
|
||||||
fileSizeLimit: this.sizeLimit,
|
fileSizeLimit: this.sizeLimit,
|
||||||
|
objectName: this.initialObjectName,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ export default {
|
|||||||
const response = await uploader.upload(file)
|
const response = await uploader.upload(file)
|
||||||
if (uploadResponseOkay(response)) {
|
if (uploadResponseOkay(response)) {
|
||||||
this.attachment = e.target.value
|
this.attachment = e.target.value
|
||||||
|
this.objectName = uploader.objectName
|
||||||
this.$refs.attachmentFilename.value = file.name
|
this.$refs.attachmentFilename.value = file.name
|
||||||
this.$refs.attachmentObjectName.value = response.objectName
|
this.$refs.attachmentObjectName.value = response.objectName
|
||||||
this.$refs.attachmentInput.disabled = true
|
this.$refs.attachmentInput.disabled = true
|
||||||
|
12
js/index.js
12
js/index.js
@ -32,12 +32,14 @@ import ToForm from './components/forms/to_form'
|
|||||||
import ClinFields from './components/clin_fields'
|
import ClinFields from './components/clin_fields'
|
||||||
import PopDateRange from './components/pop_date_range'
|
import PopDateRange from './components/pop_date_range'
|
||||||
import ToggleMenu from './components/toggle_menu'
|
import ToggleMenu from './components/toggle_menu'
|
||||||
|
import ExpandSidenav from './mixins/expand_sidenav'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
Vue.use(VTooltip)
|
Vue.use(VTooltip)
|
||||||
|
|
||||||
Vue.mixin(Modal)
|
Vue.mixin(Modal)
|
||||||
|
Vue.mixin(ExpandSidenav)
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
el: '#app-root',
|
el: '#app-root',
|
||||||
@ -67,6 +69,12 @@ const app = new Vue({
|
|||||||
ToggleMenu,
|
ToggleMenu,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
sidenavExpanded: this.defaultVisible,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
this.$on('modalOpen', data => {
|
this.$on('modalOpen', data => {
|
||||||
if (data['isOpen']) {
|
if (data['isOpen']) {
|
||||||
@ -105,6 +113,10 @@ const app = new Vue({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.$on('sidenavToggle', data => {
|
||||||
|
this.sidenavExpanded = data
|
||||||
|
})
|
||||||
},
|
},
|
||||||
delimiters: ['!{', '}'],
|
delimiters: ['!{', '}'],
|
||||||
|
|
||||||
|
@ -9,6 +9,12 @@ export default {
|
|||||||
unmask: [],
|
unmask: [],
|
||||||
validationError: 'Please enter a response',
|
validationError: 'Please enter a response',
|
||||||
},
|
},
|
||||||
|
applicationName: {
|
||||||
|
mask: false,
|
||||||
|
match: /^[A-Za-z0-9\-_,'".\s]{4,100}$$/,
|
||||||
|
unmask: [],
|
||||||
|
validationError: 'Application names can be between 4-100 characters',
|
||||||
|
},
|
||||||
clinNumber: {
|
clinNumber: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^\d{4}$/,
|
match: /^\d{4}$/,
|
||||||
@ -42,14 +48,14 @@ export default {
|
|||||||
},
|
},
|
||||||
defaultStringField: {
|
defaultStringField: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^[A-Za-z0-9\-_ \.]{1,100}$/,
|
match: /^[A-Za-z0-9\-_,'".\s]{1,1000}$/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
validationError:
|
validationError:
|
||||||
'Please enter a response of no more than 100 alphanumeric characters',
|
'Please enter a response of no more than 100 alphanumeric characters',
|
||||||
},
|
},
|
||||||
defaultTextAreaField: {
|
defaultTextAreaField: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^[A-Za-z0-9\-_ \.]{1,1000}$/,
|
match: /^[A-Za-z0-9\-_,'".\s]{1,1000}$/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
validationError:
|
validationError:
|
||||||
'Please enter a response of no more than 1000 alphanumeric characters',
|
'Please enter a response of no more than 1000 alphanumeric characters',
|
||||||
@ -94,7 +100,7 @@ export default {
|
|||||||
},
|
},
|
||||||
portfolioName: {
|
portfolioName: {
|
||||||
mask: false,
|
mask: false,
|
||||||
match: /^.{4,100}$/,
|
match: /^[A-Za-z0-9\-_,'".\s]{4,100}$$/,
|
||||||
unmask: [],
|
unmask: [],
|
||||||
validationError: 'Portfolio names can be between 4-100 characters',
|
validationError: 'Portfolio names can be between 4-100 characters',
|
||||||
},
|
},
|
||||||
|
15
js/mixins/expand_sidenav.js
Normal file
15
js/mixins/expand_sidenav.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
cookieName: 'expandSidenav',
|
||||||
|
defaultVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: function() {
|
||||||
|
if (document.cookie.match(this.cookieName)) {
|
||||||
|
return !!document.cookie.match(this.cookieName + ' *= *true')
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -47,3 +47,4 @@
|
|||||||
@import "sections/application_edit";
|
@import "sections/application_edit";
|
||||||
@import "sections/reports";
|
@import "sections/reports";
|
||||||
@import "sections/task_order";
|
@import "sections/task_order";
|
||||||
|
@import "sections/ccpo";
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
.usa-alert {
|
.usa-alert {
|
||||||
padding-bottom: 2.4rem;
|
padding-bottom: 2.4rem;
|
||||||
|
max-width: $max-panel-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin alert {
|
@mixin alert {
|
||||||
@ -97,38 +98,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
|
||||||
@include alert;
|
|
||||||
@include alert-level("info");
|
|
||||||
|
|
||||||
&.alert--success {
|
|
||||||
@include alert-level("success");
|
|
||||||
|
|
||||||
.alert__actions {
|
|
||||||
.icon-link {
|
|
||||||
@include icon-link-color($color-green, $color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.alert--warning {
|
|
||||||
@include alert-level("warning");
|
|
||||||
|
|
||||||
.alert__actions {
|
|
||||||
.icon-link {
|
|
||||||
@include icon-link-color($color-gold-dark, $color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.alert--error {
|
|
||||||
@include alert-level("error");
|
|
||||||
|
|
||||||
.alert__actions {
|
|
||||||
.icon-link {
|
|
||||||
@include icon-link-color($color-red, $color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
.error-page {
|
.error-page {
|
||||||
max-width: 475px;
|
max-width: $max-page-width;
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: unset;
|
||||||
|
border: none;
|
||||||
|
max-width: 475px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
&__heading {
|
&__heading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $gap 0;
|
padding: $gap 0;
|
||||||
@ -15,17 +20,6 @@
|
|||||||
margin-bottom: $gap;
|
margin-bottom: $gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__body {
|
|
||||||
padding: $gap * 2;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
hr {
|
|
||||||
width: 80%;
|
|
||||||
margin: auto;
|
|
||||||
margin-bottom: $gap * 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
max-width: 1190px;
|
max-width: $max-page-width;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
|
@ -98,3 +98,7 @@ hr {
|
|||||||
.usa-section {
|
.usa-section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
margin-bottom: $action-footer-height + $large-spacing;
|
||||||
|
}
|
||||||
|
@ -19,6 +19,8 @@ $sidenav-collapsed-width: 10rem;
|
|||||||
$max-panel-width: 90rem;
|
$max-panel-width: 90rem;
|
||||||
$home-pg-icon-width: 6rem;
|
$home-pg-icon-width: 6rem;
|
||||||
$large-spacing: 4rem;
|
$large-spacing: 4rem;
|
||||||
|
$max-page-width: $max-panel-width + $sidenav-expanded-width + $large-spacing;
|
||||||
|
$action-footer-height: 6rem;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* USWDS Variables
|
* USWDS Variables
|
||||||
|
@ -32,22 +32,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-group-footer {
|
.action-group-footer {
|
||||||
@extend .action-group;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-top: $gap;
|
padding-top: $gap;
|
||||||
padding-bottom: $gap;
|
padding-bottom: $gap;
|
||||||
|
padding-right: $gap * 4;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: $footer-height;
|
bottom: $footer-height;
|
||||||
|
left: 0;
|
||||||
background: white;
|
background: white;
|
||||||
right: 0;
|
|
||||||
padding-right: $gap * 4;
|
|
||||||
border-top: 1px solid $color-gray-lighter;
|
border-top: 1px solid $color-gray-lighter;
|
||||||
width: 100%;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: $action-footer-height;
|
||||||
|
|
||||||
|
&.action-group-footer--expand-offset {
|
||||||
|
padding-left: $sidenav-expanded-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-group-footer--collapse-offset {
|
||||||
|
padding-left: $sidenav-collapsed-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group-footer--container {
|
||||||
|
@extend .action-group;
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: $large-spacing;
|
||||||
|
max-width: $max-panel-width;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,6 +228,7 @@
|
|||||||
|
|
||||||
&--validation {
|
&--validation {
|
||||||
&--anything,
|
&--anything,
|
||||||
|
&--applicationName,
|
||||||
&--portfolioName,
|
&--portfolioName,
|
||||||
&--requiredField,
|
&--requiredField,
|
||||||
&--defaultStringField,
|
&--defaultStringField,
|
||||||
|
3
styles/sections/_ccpo.scss
Normal file
3
styles/sections/_ccpo.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.ccpo-panel-container {
|
||||||
|
max-width: $max-panel-width;
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
.task-order {
|
.task-order {
|
||||||
margin-top: $gap * 4;
|
margin-top: $gap * 4;
|
||||||
margin-bottom: $footer-height;
|
|
||||||
width: 900px;
|
width: 900px;
|
||||||
|
|
||||||
&__amount {
|
&__amount {
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ env['name'] }}
|
{{ env['name'] }}
|
||||||
</span>
|
</span>
|
||||||
{{ Label(type="pending_creation", classes='label--below')}}
|
{{ Label(type="pending_creation")}}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% if user_can(permissions.EDIT_ENVIRONMENT) -%}
|
{% if user_can(permissions.EDIT_ENVIRONMENT) -%}
|
||||||
{{
|
{{
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
action_new,
|
action_new,
|
||||||
action_update) %}
|
action_update) %}
|
||||||
|
|
||||||
<h3 id="application-members">
|
<h3 id="application-members">
|
||||||
{{ 'portfolios.applications.settings.team_members' | translate }}
|
{{ 'portfolios.applications.settings.team_members' | translate }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@ -22,7 +22,7 @@
|
|||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel{% if new_member_form%} form{% endif %}">
|
||||||
{% if not application.members %}
|
{% if not application.members %}
|
||||||
<div class='empty-state empty-state--centered empty-state--white panel__content'>
|
<div class='empty-state empty-state--centered empty-state--white panel__content'>
|
||||||
<p class='empty-state__message'>
|
<p class='empty-state__message'>
|
||||||
|
@ -22,11 +22,11 @@
|
|||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<base-form inline-template :enable-save="true">
|
<base-form inline-template :enable-save="true">
|
||||||
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit">
|
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit" class="form">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-col">
|
<div class="form-col">
|
||||||
{{ TextInput(form.name, validation="name", optional=False) }}
|
{{ TextInput(form.name, validation="applicationName", optional=False) }}
|
||||||
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
|
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -39,14 +39,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<span class="action-group-footer">
|
<div
|
||||||
{% block next_button %}
|
class="action-group-footer"
|
||||||
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
{% endblock %}
|
<div class="action-group-footer--container">
|
||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
{% block next_button %}
|
||||||
Cancel
|
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
||||||
</a>
|
{% endblock %}
|
||||||
</span>
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</base-form>
|
</base-form>
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||||
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit">
|
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit" class="form">
|
||||||
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
|
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
@ -58,20 +58,24 @@
|
|||||||
{{ Icon("plus") }}
|
{{ Icon("plus") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="action-group-footer"
|
||||||
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
|
<div class="action-group-footer--container">
|
||||||
|
{% block next_button %}
|
||||||
|
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
||||||
|
{% endblock %}
|
||||||
|
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_1', application_id=application.id) }}">
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<span class="action-group-footer">
|
|
||||||
{% block next_button %}
|
|
||||||
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
|
||||||
{% endblock %}
|
|
||||||
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_1', application_id=application.id) }}">
|
|
||||||
Previous
|
|
||||||
</a>
|
|
||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</form>
|
</form>
|
||||||
</application-environments>
|
</application-environments>
|
||||||
|
|
||||||
|
@ -25,16 +25,20 @@
|
|||||||
action_update="applications.update_new_application_step_3") }}
|
action_update="applications.update_new_application_step_3") }}
|
||||||
|
|
||||||
|
|
||||||
<span class="action-group-footer">
|
<div
|
||||||
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
|
class="action-group-footer"
|
||||||
{{ "portfolios.applications.new.step_3_button_text" | translate }}
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
</a>
|
<div class="action-group-footer--container">
|
||||||
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_2', application_id=application.id) }}">
|
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
|
||||||
{{ "common.previous" | translate }}
|
{{ "portfolios.applications.new.step_3_button_text" | translate }}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_2', application_id=application.id) }}">
|
||||||
{{ "common.cancel" | translate }}
|
{{ "common.previous" | translate }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
|
{{ "common.cancel" | translate }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<base-form inline-template>
|
<base-form inline-template>
|
||||||
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="col col--half">
|
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="col col--half">
|
||||||
{{ application_form.csrf_token }}
|
{{ application_form.csrf_token }}
|
||||||
{{ TextInput(application_form.name, validation="name", optional=False) }}
|
{{ TextInput(application_form.name, validation="applicationName", optional=False) }}
|
||||||
{{ TextInput(application_form.description, validation="defaultTextAreaField", paragraph=True, optional=True, showOptional=False) }}
|
{{ TextInput(application_form.description, validation="defaultTextAreaField", paragraph=True, optional=True, showOptional=False) }}
|
||||||
<div class="action-group action-group--tight">
|
<div class="action-group action-group--tight">
|
||||||
{{ SaveButton(text='common.save_changes'|translate) }}
|
{{ SaveButton(text='common.save_changes'|translate) }}
|
||||||
|
@ -4,21 +4,23 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<base-form inline-template>
|
<div class="ccpo-panel-container">
|
||||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.submit_new_user') }}" method="POST">
|
<base-form inline-template>
|
||||||
{{ form.csrf_token }}
|
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.submit_new_user') }}" method="POST">
|
||||||
<h1>{{ "ccpo.form.add_user_title" | translate }}</h1>
|
{{ form.csrf_token }}
|
||||||
<div class='form-row'>
|
<h1>{{ "ccpo.form.add_user_title" | translate }}</h1>
|
||||||
<div class='form-col form-col--two-thirds'>
|
<div class='form-row'>
|
||||||
{{ TextInput(form.dod_id, validation='dodId', optional=False) }}
|
<div class='form-col form-col--two-thirds'>
|
||||||
</div>
|
{{ TextInput(form.dod_id, validation='dodId', optional=False) }}
|
||||||
<div class="form-col form-col--third">
|
</div>
|
||||||
<div class='action-group'>
|
<div class="form-col form-col--third">
|
||||||
{{ SaveButton(text="common.next"|translate, element="input", additional_classes="action-group__action", form="add-ccpo-user-form") }}
|
<div class='action-group'>
|
||||||
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">{{ "common.cancel" | translate }}</a>
|
{{ SaveButton(text="common.next"|translate, element="input", additional_classes="action-group__action", form="add-ccpo-user-form") }}
|
||||||
|
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">{{ "common.cancel" | translate }}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</base-form>
|
||||||
</base-form>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,31 +3,33 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if new_user %}
|
<div class="ccpo-panel-container">
|
||||||
<h3>{{ 'ccpo.form.confirm_user_title' | translate }}</h3>
|
{% if new_user %}
|
||||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.confirm_new_user') }}" method="POST">
|
<h3>{{ 'ccpo.form.confirm_user_title' | translate }}</h3>
|
||||||
{{ form.csrf_token }}
|
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.confirm_new_user') }}" method="POST">
|
||||||
<input type="hidden" name="dod_id" value="{{ form.dod_id.data }}">
|
{{ form.csrf_token }}
|
||||||
<div>
|
<input type="hidden" name="dod_id" value="{{ form.dod_id.data }}">
|
||||||
<p>
|
<div>
|
||||||
{{ "ccpo.form.confirm_user_text" | translate }}
|
<p>
|
||||||
</p>
|
{{ "ccpo.form.confirm_user_text" | translate }}
|
||||||
<p>
|
</p>
|
||||||
{{ new_user.full_name }}
|
<p>
|
||||||
</p>
|
{{ new_user.full_name }}
|
||||||
<p>
|
</p>
|
||||||
{{ new_user.email }}
|
<p>
|
||||||
</p>
|
{{ new_user.email }}
|
||||||
</div>
|
</p>
|
||||||
<div class='action-group'>
|
</div>
|
||||||
<input
|
<div class='action-group'>
|
||||||
type='submit'
|
<input
|
||||||
class='action-group__action usa-button'
|
type='submit'
|
||||||
value='{{ "ccpo.form.confirm_button" | translate }}'>
|
class='action-group__action usa-button'
|
||||||
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">
|
value='{{ "ccpo.form.confirm_button" | translate }}'>
|
||||||
{{ "common.cancel" | translate }}
|
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">
|
||||||
</a>
|
{{ "common.cancel" | translate }}
|
||||||
</div>
|
</a>
|
||||||
</form>
|
</div>
|
||||||
{% endif %}
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,78 +6,80 @@
|
|||||||
{% from "components/modal.html" import Modal %}
|
{% from "components/modal.html" import Modal %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class='col'>
|
<div class="ccpo-panel-container">
|
||||||
<div class="h2">
|
<div class='col'>
|
||||||
{{ "ccpo.users_title" | translate }}
|
<div class="h2">
|
||||||
</div>
|
{{ "ccpo.users_title" | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ "common.name" | translate }}</th>
|
|
||||||
<th>{{ "common.email" | translate }}</th>
|
|
||||||
<th>{{ "common.dod_id" | translate }}</th>
|
|
||||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
|
||||||
<th></th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for user, form in users_info %}
|
|
||||||
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
|
||||||
{% set disable_button_class = 'button-danger-outline' %}
|
|
||||||
{% if user == g.current_user %}
|
|
||||||
{% set disable_button_class = "usa-button-disabled" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.full_name }}</td>
|
<th>{{ "common.name" | translate }}</th>
|
||||||
<td>{{ user.email }}</td>
|
<th>{{ "common.email" | translate }}</th>
|
||||||
<td>{{ user.dod_id }}</td>
|
<th>{{ "common.dod_id" | translate }}</th>
|
||||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||||
<td>
|
<th></th>
|
||||||
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ disable_button_class }}'>
|
|
||||||
{{ "common.disable" | translate }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{% for user, form in users_info %}
|
||||||
|
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
||||||
|
{% set disable_button_class = 'button-danger-outline' %}
|
||||||
|
{% if user == g.current_user %}
|
||||||
|
{% set disable_button_class = "usa-button-disabled" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.full_name }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>{{ user.dod_id }}</td>
|
||||||
|
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||||
|
<td>
|
||||||
|
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ disable_button_class }}'>
|
||||||
|
{{ "common.disable" | translate }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user_can(permissions.CREATE_CCPO_USER) %}
|
||||||
|
<a class="icon-link" href="{{ url_for('ccpo.add_new_user')}}">
|
||||||
|
{{ "ccpo.add_user" | translate }} {{ Icon("plus") }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||||
|
{% for user, form in users_info %}
|
||||||
|
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
||||||
|
{% call Modal(name=modal_id) %}
|
||||||
|
<h1>Disable CCPO User</h1>
|
||||||
|
<hr>
|
||||||
|
{{
|
||||||
|
Alert(
|
||||||
|
title=("components.modal.destructive_title" | translate),
|
||||||
|
message=("ccpo.disable_user.alert_message" | translate({"user_name": user.full_name})),
|
||||||
|
level="warning"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
{{
|
||||||
|
DeleteConfirmation(
|
||||||
|
modal_id=modal_id,
|
||||||
|
delete_text='Remove Access',
|
||||||
|
delete_action=(url_for('ccpo.remove_access', user_id=user.id)),
|
||||||
|
form=form,
|
||||||
|
confirmation_text='remove'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
{% endcall %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user_can(permissions.CREATE_CCPO_USER) %}
|
|
||||||
<a class="icon-link" href="{{ url_for('ccpo.add_new_user')}}">
|
|
||||||
{{ "ccpo.add_user" | translate }} {{ Icon("plus") }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
|
||||||
{% for user, form in users_info %}
|
|
||||||
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
|
||||||
{% call Modal(name=modal_id) %}
|
|
||||||
<h1>Disable CCPO User</h1>
|
|
||||||
<hr>
|
|
||||||
{{
|
|
||||||
Alert(
|
|
||||||
title=("components.modal.destructive_title" | translate),
|
|
||||||
message=("ccpo.disable_user.alert_message" | translate({"user_name": user.full_name})),
|
|
||||||
level="warning"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{{
|
|
||||||
DeleteConfirmation(
|
|
||||||
modal_id=modal_id,
|
|
||||||
delete_text='Remove Access',
|
|
||||||
delete_action=(url_for('ccpo.remove_access', user_id=user.id)),
|
|
||||||
form=form,
|
|
||||||
confirmation_text='remove'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{% endcall %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
inline-template
|
inline-template
|
||||||
{% if not field.errors %}
|
{% if not field.errors %}
|
||||||
v-bind:filename='{{ field.filename.data | tojson }}'
|
v-bind:filename='{{ field.filename.data | tojson }}'
|
||||||
v-bind:object-name='{{ field.object_name.data | tojson }}'
|
v-bind:initial-object-name='{{ field.object_name.data | tojson }}'
|
||||||
{% else %}
|
{% else %}
|
||||||
v-bind:initial-errors='true'
|
v-bind:initial-errors='true'
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -46,7 +46,7 @@
|
|||||||
v-bind:value="attachment"
|
v-bind:value="attachment"
|
||||||
type="file">
|
type="file">
|
||||||
<input type="hidden" name="{{ field.filename.name }}" id="{{ field.filename.name }}" ref="attachmentFilename">
|
<input type="hidden" name="{{ field.filename.name }}" id="{{ field.filename.name }}" ref="attachmentFilename">
|
||||||
<input type="hidden" name="{{ field.object_name.name }}" id="{{ field.object_name.name }}" ref="attachmentObjectName">
|
<input type="hidden" name="{{ field.object_name.name }}" id="{{ field.object_name.name }}" ref="attachmentObjectName" v-bind:value='objectName'>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="uploadError">
|
<template v-if="uploadError">
|
||||||
<span class="usa-input__message">{{ "forms.task_order.upload_error" | translate }}</span>
|
<span class="usa-input__message">{{ "forms.task_order.upload_error" | translate }}</span>
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<main class="usa-section usa-content error-page">
|
<main class="usa-section usa-content error-page">
|
||||||
|
<div class="panel">
|
||||||
<div class="panel__heading">
|
<div class="panel__heading">
|
||||||
{{ Icon('cloud', classes="icon--red icon--large")}}
|
{{ Icon('cloud', classes="icon--red icon--large")}}
|
||||||
<hr>
|
<hr>
|
||||||
@ -17,6 +18,7 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -10,29 +10,30 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }} " />
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }} " />
|
||||||
</head>
|
</head>
|
||||||
<body class="{% if g.modalOpen %} modalOpen{% endif %}">
|
<body class="{% if g.modalOpen %} modalOpen{% endif %}">
|
||||||
|
<div id='app-root'>
|
||||||
|
{% block template_vars %}{% endblock %}
|
||||||
|
|
||||||
{% block template_vars %}{% endblock %}
|
{% include 'components/usa_header.html' %}
|
||||||
|
|
||||||
{% include 'components/usa_header.html' %}
|
{% include 'navigation/topbar.html' %}
|
||||||
|
|
||||||
{% include 'navigation/topbar.html' %}
|
<div class='global-layout'>
|
||||||
|
|
||||||
<div class='global-layout'>
|
<div class='global-panel-container'>
|
||||||
|
{% block sidenav %}{% endblock %}
|
||||||
|
|
||||||
<div class='global-panel-container'>
|
{% block content %}
|
||||||
{% block sidenav %}{% endblock %}
|
these are not the droids you are looking for
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
</div>
|
||||||
these are not the droids you are looking for
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'footer.html' %}
|
||||||
|
|
||||||
|
{% block modal %}{% endblock %}
|
||||||
|
{% assets "js_all" %}
|
||||||
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
|
{% endassets %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
|
||||||
|
|
||||||
{% block modal %}{% endblock %}
|
|
||||||
{% assets "js_all" %}
|
|
||||||
<script src="{{ ASSET_URL }}"></script>
|
|
||||||
{% endassets %}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -15,14 +15,15 @@
|
|||||||
<p>{{ "portfolios.header" | translate }}</p>
|
<p>{{ "portfolios.header" | translate }}</p>
|
||||||
<h1>{{ 'portfolios.new.title' | translate }}</h1>
|
<h1>{{ 'portfolios.new.title' | translate }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
||||||
<base-form inline-template>
|
<base-form inline-template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<form id="portfolio-create" class="col" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
<form id="portfolio-create" class="col form" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div class="form-row form-row--bordered">
|
<div class="form-row form-row--bordered">
|
||||||
<div class="form-col">
|
<div class="form-col">
|
||||||
{{ TextInput(form.name, validation="name", optional=False, classes="form-col") }}
|
{{ TextInput(form.name, validation="portfolioName", optional=False, classes="form-col") }}
|
||||||
{{"forms.portfolio.name.help_text" | translate | safe }}
|
{{"forms.portfolio.name.help_text" | translate | safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -38,13 +39,18 @@
|
|||||||
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='action-group-footer'>
|
<div
|
||||||
{% block next_button %}
|
class='action-group-footer'
|
||||||
{{ SaveButton(text=('portfolios.new.save' | translate), form="portfolio-create", element="input") }}
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
{% endblock %}
|
<div class="action-group-footer--container">
|
||||||
<a class="usa-button usa-button-secondary" href="{{ url_for('atst.home') }}">
|
{% block next_button %}
|
||||||
Cancel
|
{{ SaveButton(text=('portfolios.new.save' | translate), form="portfolio-create", element="input") }}
|
||||||
</a>
|
{% endblock %}
|
||||||
|
<a class="usa-button usa-button-secondary" href="{{ url_for('atst.home') }}">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</base-form>
|
</base-form>
|
||||||
|
@ -28,10 +28,13 @@
|
|||||||
|
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<div class="task-order">
|
<div class="task-order form">
|
||||||
{% block to_builder_form_field %}{% endblock %}
|
{% block to_builder_form_field %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<span class="action-group-footer">
|
<div
|
||||||
|
class="action-group-footer"
|
||||||
|
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||||
|
<div class="action-group-footer--container">
|
||||||
{% block next_button %}
|
{% block next_button %}
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -58,14 +61,13 @@
|
|||||||
</a>
|
</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
v-on:click="openModal('cancel')"
|
v-on:click="openModal('cancel')"
|
||||||
class="action-group__action icon-link">
|
class="action-group__action icon-link">
|
||||||
{{ "common.cancel" | translate }}
|
{{ "common.cancel" | translate }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</to-form>
|
</to-form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -2,6 +2,8 @@ import json
|
|||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pendulum
|
||||||
|
import pydantic
|
||||||
import pytest
|
import pytest
|
||||||
from tests.factories import ApplicationFactory, EnvironmentFactory
|
from tests.factories import ApplicationFactory, EnvironmentFactory
|
||||||
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
|
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
|
||||||
@ -20,10 +22,16 @@ from atst.domain.csp.cloud.models import (
|
|||||||
BillingProfileTenantAccessCSPResult,
|
BillingProfileTenantAccessCSPResult,
|
||||||
BillingProfileVerificationCSPPayload,
|
BillingProfileVerificationCSPPayload,
|
||||||
BillingProfileVerificationCSPResult,
|
BillingProfileVerificationCSPResult,
|
||||||
|
CostManagementQueryCSPResult,
|
||||||
|
EnvironmentCSPPayload,
|
||||||
|
EnvironmentCSPResult,
|
||||||
|
PrincipalAdminRoleCSPPayload,
|
||||||
|
PrincipalAdminRoleCSPResult,
|
||||||
ProductPurchaseCSPPayload,
|
ProductPurchaseCSPPayload,
|
||||||
ProductPurchaseCSPResult,
|
ProductPurchaseCSPResult,
|
||||||
ProductPurchaseVerificationCSPPayload,
|
ProductPurchaseVerificationCSPPayload,
|
||||||
ProductPurchaseVerificationCSPResult,
|
ProductPurchaseVerificationCSPResult,
|
||||||
|
ReportingCSPPayload,
|
||||||
SubscriptionCreationCSPPayload,
|
SubscriptionCreationCSPPayload,
|
||||||
SubscriptionCreationCSPResult,
|
SubscriptionCreationCSPResult,
|
||||||
SubscriptionVerificationCSPPayload,
|
SubscriptionVerificationCSPPayload,
|
||||||
@ -44,6 +52,7 @@ from atst.domain.csp.cloud.models import (
|
|||||||
TenantPrincipalCSPResult,
|
TenantPrincipalCSPResult,
|
||||||
TenantPrincipalOwnershipCSPPayload,
|
TenantPrincipalOwnershipCSPPayload,
|
||||||
TenantPrincipalOwnershipCSPResult,
|
TenantPrincipalOwnershipCSPResult,
|
||||||
|
UserCSPPayload,
|
||||||
)
|
)
|
||||||
|
|
||||||
BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31"
|
BILLING_ACCOUNT_NAME = "52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31"
|
||||||
@ -57,12 +66,14 @@ def mock_management_group_create(mock_azure, spec_dict):
|
|||||||
|
|
||||||
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
||||||
environment = EnvironmentFactory.create()
|
environment = EnvironmentFactory.create()
|
||||||
|
|
||||||
mock_management_group_create(mock_azure, {"id": "Test Id"})
|
mock_management_group_create(mock_azure, {"id": "Test Id"})
|
||||||
|
|
||||||
result = mock_azure.create_environment(
|
mock_azure = mock_get_secret(mock_azure)
|
||||||
AUTH_CREDENTIALS, environment.creator, environment
|
|
||||||
|
payload = EnvironmentCSPPayload(
|
||||||
|
tenant_id="1234", display_name=environment.name, parent_id=str(uuid4())
|
||||||
)
|
)
|
||||||
|
result = mock_azure.create_environment(payload)
|
||||||
|
|
||||||
assert result.id == "Test Id"
|
assert result.id == "Test Id"
|
||||||
|
|
||||||
@ -97,20 +108,6 @@ def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
|||||||
assert result.id == "Test Id"
|
assert result.id == "Test Id"
|
||||||
|
|
||||||
|
|
||||||
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):
|
|
||||||
environment_id = str(uuid4())
|
|
||||||
|
|
||||||
csp_user_id = str(uuid4)
|
|
||||||
|
|
||||||
mock_azure.sdk.graphrbac.GraphRbacManagementClient.return_value.service_principals.create.return_value.object_id = (
|
|
||||||
csp_user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
result = mock_azure.create_atat_admin_user(AUTH_CREDENTIALS, environment_id)
|
|
||||||
|
|
||||||
assert result.get("csp_user_id") == csp_user_id
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
|
def test_create_policy_definition_succeeds(mock_azure: AzureCloudProvider):
|
||||||
subscription_id = str(uuid4())
|
subscription_id = str(uuid4())
|
||||||
management_group_id = str(uuid4())
|
management_group_id = str(uuid4())
|
||||||
@ -162,6 +159,27 @@ def test_create_tenant(mock_azure: AzureCloudProvider):
|
|||||||
assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
|
assert body.tenant_id == "60ff9d34-82bf-4f21-b565-308ef0533435"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_tenant_fails(mock_azure: AzureCloudProvider):
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.json.return_value = {"error": "body"}
|
||||||
|
mock_result.status_code = 403
|
||||||
|
mock_azure.sdk.requests.post.return_value = mock_result
|
||||||
|
payload = TenantCSPPayload(
|
||||||
|
**dict(
|
||||||
|
user_id="admin",
|
||||||
|
password="JediJan13$coot", # pragma: allowlist secret
|
||||||
|
domain_name="jediccpospawnedtenant2",
|
||||||
|
first_name="Tedry",
|
||||||
|
last_name="Tenet",
|
||||||
|
country_code="US",
|
||||||
|
password_recovery_email_address="thomas@promptworks.com",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mock_azure = mock_get_secret(mock_azure)
|
||||||
|
result = mock_azure.create_tenant(payload)
|
||||||
|
assert result.get("status") == "error"
|
||||||
|
|
||||||
|
|
||||||
def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
|
def test_create_billing_profile_creation(mock_azure: AzureCloudProvider):
|
||||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||||
"accessToken": "TOKEN"
|
"accessToken": "TOKEN"
|
||||||
@ -573,10 +591,10 @@ def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider):
|
|||||||
def test_create_admin_role_definition(mock_azure: AzureCloudProvider):
|
def test_create_admin_role_definition(mock_azure: AzureCloudProvider):
|
||||||
with patch.object(
|
with patch.object(
|
||||||
AzureCloudProvider,
|
AzureCloudProvider,
|
||||||
"_get_elevated_management_token",
|
"_get_tenant_admin_token",
|
||||||
wraps=mock_azure._get_elevated_management_token,
|
wraps=mock_azure._get_tenant_admin_token,
|
||||||
) as get_elevated_management_token:
|
) as get_tenant_admin_token:
|
||||||
get_elevated_management_token.return_value = "my fake token"
|
get_tenant_admin_token.return_value = "my fake token"
|
||||||
|
|
||||||
mock_result = Mock()
|
mock_result = Mock()
|
||||||
mock_result.ok = True
|
mock_result.ok = True
|
||||||
@ -657,6 +675,35 @@ def test_create_tenant_principal_ownership(mock_azure: AzureCloudProvider):
|
|||||||
assert result.principal_owner_assignment_id == "id"
|
assert result.principal_owner_assignment_id == "id"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_principal_admin_role(mock_azure: AzureCloudProvider):
|
||||||
|
with patch.object(
|
||||||
|
AzureCloudProvider,
|
||||||
|
"_get_tenant_admin_token",
|
||||||
|
wraps=mock_azure._get_tenant_admin_token,
|
||||||
|
) as get_tenant_admin_token:
|
||||||
|
get_tenant_admin_token.return_value = "my fake token"
|
||||||
|
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.ok = True
|
||||||
|
mock_result.json.return_value = {"id": "id"}
|
||||||
|
|
||||||
|
mock_azure.sdk.requests.post.return_value = mock_result
|
||||||
|
|
||||||
|
payload = PrincipalAdminRoleCSPPayload(
|
||||||
|
**{
|
||||||
|
"tenant_id": uuid4().hex,
|
||||||
|
"principal_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
|
||||||
|
"admin_role_def_id": uuid4().hex,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result: PrincipalAdminRoleCSPResult = mock_azure.create_principal_admin_role(
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.principal_assignment_id == "id"
|
||||||
|
|
||||||
|
|
||||||
def test_create_subscription_creation(mock_azure: AzureCloudProvider):
|
def test_create_subscription_creation(mock_azure: AzureCloudProvider):
|
||||||
with patch.object(
|
with patch.object(
|
||||||
AzureCloudProvider,
|
AzureCloudProvider,
|
||||||
@ -718,3 +765,176 @@ def test_create_subscription_verification(mock_azure: AzureCloudProvider):
|
|||||||
payload
|
payload
|
||||||
)
|
)
|
||||||
assert result.subscription_id == "60fbbb72-0516-4253-ab18-c92432ba3230"
|
assert result.subscription_id == "60fbbb72-0516-4253-ab18-c92432ba3230"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_reporting_data(mock_azure: AzureCloudProvider):
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.json.return_value = {
|
||||||
|
"eTag": None,
|
||||||
|
"id": "providers/Microsoft.Billing/billingAccounts/52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31/billingProfiles/XQDJ-6LB4-BG7-TGB/invoiceSections/P73M-XC7J-PJA-TGB/providers/Microsoft.CostManagement/query/e82d0cda-2ffb-4476-a98a-425c83c216f9",
|
||||||
|
"location": None,
|
||||||
|
"name": "e82d0cda-2ffb-4476-a98a-425c83c216f9",
|
||||||
|
"properties": {
|
||||||
|
"columns": [
|
||||||
|
{"name": "PreTaxCost", "type": "Number"},
|
||||||
|
{"name": "UsageDate", "type": "Number"},
|
||||||
|
{"name": "InvoiceId", "type": "String"},
|
||||||
|
{"name": "Currency", "type": "String"},
|
||||||
|
],
|
||||||
|
"nextLink": None,
|
||||||
|
"rows": [],
|
||||||
|
},
|
||||||
|
"sku": None,
|
||||||
|
"type": "Microsoft.CostManagement/query",
|
||||||
|
}
|
||||||
|
mock_result.ok = True
|
||||||
|
mock_azure.sdk.requests.post.return_value = mock_result
|
||||||
|
mock_azure = mock_get_secret(mock_azure)
|
||||||
|
|
||||||
|
# Subset of a profile's CSP data that we care about for reporting
|
||||||
|
csp_data = {
|
||||||
|
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
|
||||||
|
"billing_profile_properties": {
|
||||||
|
"invoice_sections": [
|
||||||
|
{
|
||||||
|
"invoice_section_id": "providers/Microsoft.Billing/billingAccounts/52865e4c-52e8-5a6c-da6b-c58f0814f06f:7ea5de9d-b8ce-4901-b1c5-d864320c7b03_2019-05-31/billingProfiles/XQDJ-6LB4-BG7-TGB/invoiceSections/P73M-XC7J-PJA-TGB",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data: CostManagementQueryCSPResult = mock_azure.get_reporting_data(
|
||||||
|
ReportingCSPPayload(
|
||||||
|
from_date=pendulum.now().subtract(years=1).add(days=1).format("YYYY-MM-DD"),
|
||||||
|
to_date=pendulum.now().format("YYYY-MM-DD"),
|
||||||
|
**csp_data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(data, CostManagementQueryCSPResult)
|
||||||
|
assert data.name == "e82d0cda-2ffb-4476-a98a-425c83c216f9"
|
||||||
|
assert len(data.properties.columns) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_reporting_data_malformed_payload(mock_azure: AzureCloudProvider):
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.ok = True
|
||||||
|
mock_azure.sdk.requests.post.return_value = mock_result
|
||||||
|
mock_azure = mock_get_secret(mock_azure)
|
||||||
|
|
||||||
|
# Malformed csp_data payloads that should throw pydantic validation errors
|
||||||
|
index_error = {
|
||||||
|
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
|
||||||
|
"billing_profile_properties": {"invoice_sections": [],},
|
||||||
|
}
|
||||||
|
key_error = {
|
||||||
|
"tenant_id": "6d2d2d6c-a6d6-41e1-8bb1-73d11475f8f4",
|
||||||
|
"billing_profile_properties": {"invoice_sections": [{}],},
|
||||||
|
}
|
||||||
|
|
||||||
|
for malformed_payload in [key_error, index_error]:
|
||||||
|
with pytest.raises(pydantic.ValidationError):
|
||||||
|
assert mock_azure.get_reporting_data(
|
||||||
|
ReportingCSPPayload(
|
||||||
|
from_date="foo", to_date="bar", **malformed_payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_secret(mock_azure: AzureCloudProvider):
|
||||||
|
with patch.object(
|
||||||
|
AzureCloudProvider,
|
||||||
|
"_get_client_secret_credential_obj",
|
||||||
|
wraps=mock_azure._get_client_secret_credential_obj,
|
||||||
|
) as _get_client_secret_credential_obj:
|
||||||
|
_get_client_secret_credential_obj.return_value = {}
|
||||||
|
|
||||||
|
mock_azure.sdk.secrets.SecretClient.return_value.get_secret.return_value.value = (
|
||||||
|
"my secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_azure.get_secret("secret key") == "my secret"
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_secret(mock_azure: AzureCloudProvider):
|
||||||
|
with patch.object(
|
||||||
|
AzureCloudProvider,
|
||||||
|
"_get_client_secret_credential_obj",
|
||||||
|
wraps=mock_azure._get_client_secret_credential_obj,
|
||||||
|
) as _get_client_secret_credential_obj:
|
||||||
|
_get_client_secret_credential_obj.return_value = {}
|
||||||
|
|
||||||
|
mock_azure.sdk.secrets.SecretClient.return_value.set_secret.return_value = (
|
||||||
|
"my secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_azure.set_secret("secret key", "secret_value") == "my secret"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_active_directory_user(mock_azure: AzureCloudProvider):
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.ok = True
|
||||||
|
mock_result.json.return_value = {"id": "id"}
|
||||||
|
mock_azure.sdk.requests.post.return_value = mock_result
|
||||||
|
|
||||||
|
payload = UserCSPPayload(
|
||||||
|
tenant_id=uuid4().hex,
|
||||||
|
display_name="Test Testerson",
|
||||||
|
tenant_host_name="testtenant",
|
||||||
|
email="test@testerson.test",
|
||||||
|
password="asdfghjkl", # pragma: allowlist secret
|
||||||
|
)
|
||||||
|
|
||||||
|
result = mock_azure._create_active_directory_user("token", payload)
|
||||||
|
|
||||||
|
assert result.id == "id"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_active_directory_user_email(mock_azure: AzureCloudProvider):
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.ok = True
|
||||||
|
mock_azure.sdk.requests.patch.return_value = mock_result
|
||||||
|
|
||||||
|
payload = UserCSPPayload(
|
||||||
|
tenant_id=uuid4().hex,
|
||||||
|
display_name="Test Testerson",
|
||||||
|
tenant_host_name="testtenant",
|
||||||
|
email="test@testerson.test",
|
||||||
|
password="asdfghjkl", # pragma: allowlist secret
|
||||||
|
)
|
||||||
|
|
||||||
|
result = mock_azure._update_active_directory_user_email(
|
||||||
|
"token", uuid4().hex, payload
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user(mock_azure: AzureCloudProvider):
|
||||||
|
with patch.object(
|
||||||
|
AzureCloudProvider,
|
||||||
|
"_get_tenant_principal_token",
|
||||||
|
wraps=mock_azure._get_tenant_principal_token,
|
||||||
|
) as _get_tenant_principal_token:
|
||||||
|
_get_tenant_principal_token.return_value = "token"
|
||||||
|
|
||||||
|
mock_result_create = Mock()
|
||||||
|
mock_result_create.ok = True
|
||||||
|
mock_result_create.json.return_value = {"id": "id"}
|
||||||
|
mock_azure.sdk.requests.post.return_value = mock_result_create
|
||||||
|
|
||||||
|
mock_result_update = Mock()
|
||||||
|
mock_result_update.ok = True
|
||||||
|
mock_azure.sdk.requests.patch.return_value = mock_result_update
|
||||||
|
|
||||||
|
payload = UserCSPPayload(
|
||||||
|
tenant_id=uuid4().hex,
|
||||||
|
display_name="Test Testerson",
|
||||||
|
tenant_host_name="testtenant",
|
||||||
|
email="test@testerson.test",
|
||||||
|
password="asdfghjkl", # pragma: allowlist secret
|
||||||
|
)
|
||||||
|
|
||||||
|
result = mock_azure.create_user(payload)
|
||||||
|
|
||||||
|
assert result.id == "id"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from atst.domain.csp import MockCloudProvider
|
from atst.domain.csp import MockCloudProvider
|
||||||
|
from atst.domain.csp.cloud.models import EnvironmentCSPPayload, EnvironmentCSPResult
|
||||||
|
|
||||||
from tests.factories import EnvironmentFactory, EnvironmentRoleFactory, UserFactory
|
from tests.factories import EnvironmentFactory, EnvironmentRoleFactory, UserFactory
|
||||||
|
|
||||||
@ -14,20 +15,17 @@ def mock_csp():
|
|||||||
|
|
||||||
def test_create_environment(mock_csp: MockCloudProvider):
|
def test_create_environment(mock_csp: MockCloudProvider):
|
||||||
environment = EnvironmentFactory.create()
|
environment = EnvironmentFactory.create()
|
||||||
user = UserFactory.create()
|
environment.application.cloud_id = "parent_id"
|
||||||
environment_id = mock_csp.create_environment(CREDENTIALS, user, environment)
|
environment.application.portfolio.csp_data = {"tenant_id": "fake"}
|
||||||
assert isinstance(environment_id, str)
|
payload = EnvironmentCSPPayload(
|
||||||
|
**dict(
|
||||||
|
tenant_id=environment.application.portfolio.csp_data.get("tenant_id"),
|
||||||
def test_create_admin_user(mock_csp: MockCloudProvider):
|
display_name=environment.name,
|
||||||
admin_user = mock_csp.create_atat_admin_user(CREDENTIALS, "env_id")
|
parent_id=environment.application.cloud_id,
|
||||||
assert isinstance(admin_user["id"], str)
|
)
|
||||||
assert isinstance(admin_user["credentials"], dict)
|
)
|
||||||
|
result = mock_csp.create_environment(payload)
|
||||||
|
assert isinstance(result, EnvironmentCSPResult)
|
||||||
def test_create_environment_baseline(mock_csp: MockCloudProvider):
|
|
||||||
baseline = mock_csp.create_atat_admin_user(CREDENTIALS, "env_id")
|
|
||||||
assert isinstance(baseline, dict)
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_or_update_user(mock_csp: MockCloudProvider):
|
def test_create_or_update_user(mock_csp: MockCloudProvider):
|
||||||
|
@ -93,27 +93,25 @@ def test_disable_completed(application_role, environment):
|
|||||||
|
|
||||||
def test_disable_checks_env_provisioning_status(session):
|
def test_disable_checks_env_provisioning_status(session):
|
||||||
environment = EnvironmentFactory.create()
|
environment = EnvironmentFactory.create()
|
||||||
assert environment.is_pending
|
assert not environment.cloud_id
|
||||||
env_role1 = EnvironmentRoleFactory.create(environment=environment)
|
env_role1 = EnvironmentRoleFactory.create(environment=environment)
|
||||||
env_role1 = EnvironmentRoles.disable(env_role1.id)
|
env_role1 = EnvironmentRoles.disable(env_role1.id)
|
||||||
assert env_role1.disabled
|
assert env_role1.disabled
|
||||||
|
|
||||||
environment.cloud_id = "cloud-id"
|
environment.cloud_id = "cloud-id"
|
||||||
environment.root_user_info = {"credentials": "credentials"}
|
|
||||||
session.add(environment)
|
session.add(environment)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(environment)
|
session.refresh(environment)
|
||||||
|
|
||||||
assert not environment.is_pending
|
assert environment.cloud_id
|
||||||
env_role2 = EnvironmentRoleFactory.create(environment=environment)
|
env_role2 = EnvironmentRoleFactory.create(environment=environment)
|
||||||
env_role2 = EnvironmentRoles.disable(env_role2.id)
|
env_role2 = EnvironmentRoles.disable(env_role2.id)
|
||||||
assert env_role2.disabled
|
assert env_role2.disabled
|
||||||
|
|
||||||
|
|
||||||
def test_disable_checks_env_role_provisioning_status():
|
def test_disable_checks_env_role_provisioning_status():
|
||||||
environment = EnvironmentFactory.create(
|
environment = EnvironmentFactory.create(cloud_id="cloud-id")
|
||||||
cloud_id="cloud-id", root_user_info={"credentials": "credentials"}
|
environment.application.portfolio.csp_data = {"tenant_id": uuid4().hex}
|
||||||
)
|
|
||||||
env_role1 = EnvironmentRoleFactory.create(environment=environment)
|
env_role1 = EnvironmentRoleFactory.create(environment=environment)
|
||||||
assert not env_role1.csp_user_id
|
assert not env_role1.csp_user_id
|
||||||
env_role1 = EnvironmentRoles.disable(env_role1.id)
|
env_role1 = EnvironmentRoles.disable(env_role1.id)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import pendulum
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
@ -14,6 +13,7 @@ from tests.factories import (
|
|||||||
EnvironmentRoleFactory,
|
EnvironmentRoleFactory,
|
||||||
ApplicationRoleFactory,
|
ApplicationRoleFactory,
|
||||||
)
|
)
|
||||||
|
from tests.utils import EnvQueryTest
|
||||||
|
|
||||||
|
|
||||||
def test_create_environments():
|
def test_create_environments():
|
||||||
@ -119,40 +119,6 @@ def test_update_does_not_duplicate_names_within_application():
|
|||||||
Environments.update(dupe_env, name)
|
Environments.update(dupe_env, name)
|
||||||
|
|
||||||
|
|
||||||
class EnvQueryTest:
|
|
||||||
@property
|
|
||||||
def NOW(self):
|
|
||||||
return pendulum.now()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def YESTERDAY(self):
|
|
||||||
return self.NOW.subtract(days=1)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def TOMORROW(self):
|
|
||||||
return self.NOW.add(days=1)
|
|
||||||
|
|
||||||
def create_portfolio_with_clins(self, start_and_end_dates, env_data=None):
|
|
||||||
env_data = env_data or {}
|
|
||||||
return PortfolioFactory.create(
|
|
||||||
applications=[
|
|
||||||
{
|
|
||||||
"name": "Mos Eisley",
|
|
||||||
"description": "Where Han shot first",
|
|
||||||
"environments": [{"name": "thebar", **env_data}],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
task_orders=[
|
|
||||||
{
|
|
||||||
"create_clins": [
|
|
||||||
{"start_date": start_date, "end_date": end_date}
|
|
||||||
for (start_date, end_date) in start_and_end_dates
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetEnvironmentsPendingCreate(EnvQueryTest):
|
class TestGetEnvironmentsPendingCreate(EnvQueryTest):
|
||||||
def test_with_expired_clins(self, session):
|
def test_with_expired_clins(self, session):
|
||||||
self.create_portfolio_with_clins([(self.YESTERDAY, self.YESTERDAY)])
|
self.create_portfolio_with_clins([(self.YESTERDAY, self.YESTERDAY)])
|
||||||
@ -168,37 +134,16 @@ class TestGetEnvironmentsPendingCreate(EnvQueryTest):
|
|||||||
self.create_portfolio_with_clins([(self.TOMORROW, self.TOMORROW)])
|
self.create_portfolio_with_clins([(self.TOMORROW, self.TOMORROW)])
|
||||||
assert len(Environments.get_environments_pending_creation(self.NOW)) == 0
|
assert len(Environments.get_environments_pending_creation(self.NOW)) == 0
|
||||||
|
|
||||||
|
def test_with_already_provisioned_app(self, session):
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], app_data={"cloud_id": uuid4().hex}
|
||||||
|
)
|
||||||
|
assert len(Environments.get_environments_pending_creation(self.NOW)) == 1
|
||||||
|
|
||||||
def test_with_already_provisioned_env(self, session):
|
def test_with_already_provisioned_env(self, session):
|
||||||
self.create_portfolio_with_clins(
|
self.create_portfolio_with_clins(
|
||||||
[(self.YESTERDAY, self.TOMORROW)], env_data={"cloud_id": uuid4().hex}
|
[(self.YESTERDAY, self.TOMORROW)],
|
||||||
|
env_data={"cloud_id": uuid4().hex},
|
||||||
|
app_data={"cloud_id": uuid4().hex},
|
||||||
)
|
)
|
||||||
assert len(Environments.get_environments_pending_creation(self.NOW)) == 0
|
assert len(Environments.get_environments_pending_creation(self.NOW)) == 0
|
||||||
|
|
||||||
|
|
||||||
class TestGetEnvironmentsPendingAtatUserCreation(EnvQueryTest):
|
|
||||||
def test_with_provisioned_environment(self):
|
|
||||||
self.create_portfolio_with_clins(
|
|
||||||
[(self.YESTERDAY, self.TOMORROW)],
|
|
||||||
{"cloud_id": uuid4().hex, "root_user_info": {}},
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
len(Environments.get_environments_pending_atat_user_creation(self.NOW)) == 0
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_with_unprovisioned_environment(self):
|
|
||||||
self.create_portfolio_with_clins(
|
|
||||||
[(self.YESTERDAY, self.TOMORROW)],
|
|
||||||
{"cloud_id": uuid4().hex, "root_user_info": None},
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
len(Environments.get_environments_pending_atat_user_creation(self.NOW)) == 1
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_with_unprovisioned_expired_clins_environment(self):
|
|
||||||
self.create_portfolio_with_clins(
|
|
||||||
[(self.YESTERDAY, self.YESTERDAY)],
|
|
||||||
{"cloud_id": uuid4().hex, "root_user_info": None},
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
len(Environments.get_environments_pending_atat_user_creation(self.NOW)) == 0
|
|
||||||
)
|
|
||||||
|
@ -26,6 +26,7 @@ from tests.factories import (
|
|||||||
PortfolioStateMachineFactory,
|
PortfolioStateMachineFactory,
|
||||||
get_all_portfolio_permission_sets,
|
get_all_portfolio_permission_sets,
|
||||||
)
|
)
|
||||||
|
from tests.utils import EnvQueryTest
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@ -263,10 +264,44 @@ def test_create_state_machine(portfolio):
|
|||||||
assert fsm
|
assert fsm
|
||||||
|
|
||||||
|
|
||||||
def test_get_portfolios_pending_provisioning(session):
|
class TestGetPortfoliosPendingCreate(EnvQueryTest):
|
||||||
for x in range(5):
|
def test_finds_unstarted(self):
|
||||||
portfolio = PortfolioFactory.create()
|
for x in range(5):
|
||||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
if x == 2:
|
||||||
if x == 2:
|
state = "COMPLETED"
|
||||||
sm.state = FSMStates.COMPLETED
|
else:
|
||||||
assert len(Portfolios.get_portfolios_pending_provisioning()) == 4
|
state = "UNSTARTED"
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], state_machine_status=state
|
||||||
|
)
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 4
|
||||||
|
|
||||||
|
def test_finds_created(self):
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], state_machine_status="TENANT_CREATED"
|
||||||
|
)
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 1
|
||||||
|
|
||||||
|
def test_does_not_find_failed(self):
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], state_machine_status="TENANT_FAILED"
|
||||||
|
)
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 0
|
||||||
|
|
||||||
|
def test_with_expired_clins(self):
|
||||||
|
self.create_portfolio_with_clins([(self.YESTERDAY, self.YESTERDAY)])
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 0
|
||||||
|
|
||||||
|
def test_with_active_clins(self):
|
||||||
|
portfolio = self.create_portfolio_with_clins([(self.YESTERDAY, self.TOMORROW)])
|
||||||
|
Portfolios.get_portfolios_pending_provisioning(self.NOW) == [portfolio.id]
|
||||||
|
|
||||||
|
def test_with_future_clins(self):
|
||||||
|
self.create_portfolio_with_clins([(self.TOMORROW, self.TOMORROW)])
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 0
|
||||||
|
|
||||||
|
def test_with_already_provisioned_env(self):
|
||||||
|
self.create_portfolio_with_clins(
|
||||||
|
[(self.YESTERDAY, self.TOMORROW)], env_data={"cloud_id": uuid4().hex}
|
||||||
|
)
|
||||||
|
assert len(Portfolios.get_portfolios_pending_provisioning(self.NOW)) == 0
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from datetime import date, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from atst.domain.exceptions import AlreadyExistsError
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
@ -178,3 +178,21 @@ def test_allows_alphanumeric_number():
|
|||||||
|
|
||||||
for number in valid_to_numbers:
|
for number in valid_to_numbers:
|
||||||
assert TaskOrders.create(portfolio.id, number, [], None)
|
assert TaskOrders.create(portfolio.id, number, [], None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_for_send_task_order_files():
|
||||||
|
new_to = TaskOrderFactory.create(create_clins=[{}])
|
||||||
|
updated_to = TaskOrderFactory.create(
|
||||||
|
create_clins=[{"last_sent_at": datetime(2020, 2, 1)}],
|
||||||
|
pdf_last_sent_at=datetime(2020, 1, 1),
|
||||||
|
)
|
||||||
|
sent_to = TaskOrderFactory.create(
|
||||||
|
create_clins=[{"last_sent_at": datetime(2020, 1, 1)}],
|
||||||
|
pdf_last_sent_at=datetime(2020, 1, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_and_new_task_orders = TaskOrders.get_for_send_task_order_files()
|
||||||
|
assert len(updated_and_new_task_orders) == 2
|
||||||
|
assert sent_to not in updated_and_new_task_orders
|
||||||
|
assert updated_to in updated_and_new_task_orders
|
||||||
|
assert new_to in updated_and_new_task_orders
|
||||||
|
@ -322,6 +322,7 @@ class TaskOrderFactory(Base):
|
|||||||
number = factory.LazyFunction(random_task_order_number)
|
number = factory.LazyFunction(random_task_order_number)
|
||||||
signed_at = None
|
signed_at = None
|
||||||
_pdf = factory.SubFactory(AttachmentFactory)
|
_pdf = factory.SubFactory(AttachmentFactory)
|
||||||
|
pdf_last_sent_at = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create(cls, model_class, *args, **kwargs):
|
def _create(cls, model_class, *args, **kwargs):
|
||||||
@ -347,6 +348,7 @@ class CLINFactory(Base):
|
|||||||
jedi_clin_type = factory.LazyFunction(
|
jedi_clin_type = factory.LazyFunction(
|
||||||
lambda *args: random.choice(list(clin.JEDICLINType))
|
lambda *args: random.choice(list(clin.JEDICLINType))
|
||||||
)
|
)
|
||||||
|
last_sent_at = None
|
||||||
|
|
||||||
|
|
||||||
class NotificationRecipientFactory(Base):
|
class NotificationRecipientFactory(Base):
|
||||||
|
@ -51,28 +51,6 @@ def test_audit_event_for_environment_deletion(session):
|
|||||||
assert after
|
assert after
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"env_data,expected_status",
|
|
||||||
[
|
|
||||||
[
|
|
||||||
{"cloud_id": None, "root_user_info": None},
|
|
||||||
Environment.ProvisioningStatus.PENDING,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{"cloud_id": 1, "root_user_info": None},
|
|
||||||
Environment.ProvisioningStatus.PENDING,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{"cloud_id": 1, "root_user_info": {}},
|
|
||||||
Environment.ProvisioningStatus.COMPLETED,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_environment_provisioning_status(env_data, expected_status):
|
|
||||||
environment = EnvironmentFactory.create(**env_data)
|
|
||||||
assert environment.provisioning_status == expected_status
|
|
||||||
|
|
||||||
|
|
||||||
def test_environment_roles_do_not_include_deleted():
|
def test_environment_roles_do_not_include_deleted():
|
||||||
member_list = [
|
member_list = [
|
||||||
{"role_name": CSPRole.ADMIN},
|
{"role_name": CSPRole.ADMIN},
|
||||||
|
@ -2,6 +2,8 @@ import pendulum
|
|||||||
import pytest
|
import pytest
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
from smtplib import SMTPException
|
||||||
|
from azure.core.exceptions import AzureError
|
||||||
|
|
||||||
from atst.domain.csp.cloud import MockCloudProvider
|
from atst.domain.csp.cloud import MockCloudProvider
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
@ -12,25 +14,26 @@ from atst.jobs import (
|
|||||||
dispatch_create_environment,
|
dispatch_create_environment,
|
||||||
dispatch_create_application,
|
dispatch_create_application,
|
||||||
dispatch_create_user,
|
dispatch_create_user,
|
||||||
dispatch_create_atat_admin_user,
|
|
||||||
dispatch_provision_portfolio,
|
dispatch_provision_portfolio,
|
||||||
|
dispatch_send_task_order_files,
|
||||||
create_environment,
|
create_environment,
|
||||||
do_create_user,
|
do_create_user,
|
||||||
do_provision_portfolio,
|
do_provision_portfolio,
|
||||||
do_create_environment,
|
do_create_environment,
|
||||||
do_create_application,
|
do_create_application,
|
||||||
do_create_atat_admin_user,
|
|
||||||
)
|
)
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
|
ApplicationFactory,
|
||||||
|
ApplicationRoleFactory,
|
||||||
EnvironmentFactory,
|
EnvironmentFactory,
|
||||||
EnvironmentRoleFactory,
|
EnvironmentRoleFactory,
|
||||||
PortfolioFactory,
|
PortfolioFactory,
|
||||||
PortfolioStateMachineFactory,
|
PortfolioStateMachineFactory,
|
||||||
ApplicationFactory,
|
TaskOrderFactory,
|
||||||
ApplicationRoleFactory,
|
|
||||||
UserFactory,
|
UserFactory,
|
||||||
)
|
)
|
||||||
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure
|
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure
|
||||||
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True, scope="function")
|
@pytest.fixture(autouse=True, scope="function")
|
||||||
@ -94,6 +97,10 @@ tomorrow = now.add(days=1)
|
|||||||
|
|
||||||
def test_create_environment_job(session, csp):
|
def test_create_environment_job(session, csp):
|
||||||
environment = EnvironmentFactory.create()
|
environment = EnvironmentFactory.create()
|
||||||
|
environment.application.cloud_id = "parentId"
|
||||||
|
environment.application.portfolio.csp_data = {"tenant_id": "fake"}
|
||||||
|
session.add(environment)
|
||||||
|
session.commit()
|
||||||
do_create_environment(csp, environment.id)
|
do_create_environment(csp, environment.id)
|
||||||
session.refresh(environment)
|
session.refresh(environment)
|
||||||
|
|
||||||
@ -149,19 +156,11 @@ def test_create_user_job(session, csp):
|
|||||||
assert app_role.cloud_id
|
assert app_role.cloud_id
|
||||||
|
|
||||||
|
|
||||||
def test_create_atat_admin_user(csp, session):
|
|
||||||
environment = EnvironmentFactory.create(cloud_id="something")
|
|
||||||
do_create_atat_admin_user(csp, environment.id)
|
|
||||||
session.refresh(environment)
|
|
||||||
|
|
||||||
assert environment.root_user_info
|
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_create_environment(session, monkeypatch):
|
def test_dispatch_create_environment(session, monkeypatch):
|
||||||
# Given that I have a portfolio with an active CLIN and two environments,
|
# Given that I have a portfolio with an active CLIN and two environments,
|
||||||
# one of which is deleted
|
# one of which is deleted
|
||||||
portfolio = PortfolioFactory.create(
|
portfolio = PortfolioFactory.create(
|
||||||
applications=[{"environments": [{}, {}]}],
|
applications=[{"environments": [{}, {}], "cloud_id": uuid4().hex}],
|
||||||
task_orders=[
|
task_orders=[
|
||||||
{
|
{
|
||||||
"create_clins": [
|
"create_clins": [
|
||||||
@ -227,36 +226,9 @@ def test_dispatch_create_user(monkeypatch):
|
|||||||
mock.delay.assert_called_once_with(application_role_ids=[app_role.id])
|
mock.delay.assert_called_once_with(application_role_ids=[app_role.id])
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_create_atat_admin_user(session, monkeypatch):
|
|
||||||
portfolio = PortfolioFactory.create(
|
|
||||||
applications=[
|
|
||||||
{"environments": [{"cloud_id": uuid4().hex, "root_user_info": None}]}
|
|
||||||
],
|
|
||||||
task_orders=[
|
|
||||||
{
|
|
||||||
"create_clins": [
|
|
||||||
{
|
|
||||||
"start_date": pendulum.now().subtract(days=1),
|
|
||||||
"end_date": pendulum.now().add(days=1),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
mock = Mock()
|
|
||||||
monkeypatch.setattr("atst.jobs.create_atat_admin_user", mock)
|
|
||||||
environment = portfolio.applications[0].environments[0]
|
|
||||||
|
|
||||||
dispatch_create_atat_admin_user.run()
|
|
||||||
|
|
||||||
mock.delay.assert_called_once_with(environment_id=environment.id)
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_environment_no_dupes(session, celery_app, celery_worker):
|
def test_create_environment_no_dupes(session, celery_app, celery_worker):
|
||||||
portfolio = PortfolioFactory.create(
|
portfolio = PortfolioFactory.create(
|
||||||
applications=[
|
applications=[{"environments": [{"cloud_id": uuid4().hex}]}],
|
||||||
{"environments": [{"cloud_id": uuid4().hex, "root_user_info": {}}]}
|
|
||||||
],
|
|
||||||
task_orders=[
|
task_orders=[
|
||||||
{
|
{
|
||||||
"create_clins": [
|
"create_clins": [
|
||||||
@ -286,9 +258,19 @@ def test_create_environment_no_dupes(session, celery_app, celery_worker):
|
|||||||
assert environment.claimed_until == None
|
assert environment.claimed_until == None
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_provision_portfolio(
|
def test_dispatch_provision_portfolio(csp, monkeypatch):
|
||||||
csp, session, portfolio, celery_app, celery_worker, monkeypatch
|
portfolio = PortfolioFactory.create(
|
||||||
):
|
task_orders=[
|
||||||
|
{
|
||||||
|
"create_clins": [
|
||||||
|
{
|
||||||
|
"start_date": pendulum.now().subtract(days=1),
|
||||||
|
"end_date": pendulum.now().add(days=1),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||||
mock = Mock()
|
mock = Mock()
|
||||||
monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
||||||
@ -310,3 +292,84 @@ def test_provision_portfolio_create_tenant(
|
|||||||
# monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
# monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
||||||
# dispatch_provision_portfolio.run()
|
# dispatch_provision_portfolio.run()
|
||||||
# mock.delay.assert_called_once_with(portfolio_id=portfolio.id)
|
# mock.delay.assert_called_once_with(portfolio_id=portfolio.id)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Refactor the tests related to dispatch_send_task_order_files() into a class
|
||||||
|
# and separate the success test into two tests
|
||||||
|
def test_dispatch_send_task_order_files(monkeypatch, app):
|
||||||
|
mock = Mock()
|
||||||
|
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||||
|
|
||||||
|
def _download_task_order(MockFileService, object_name):
|
||||||
|
return {"name": object_name}
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||||
|
_download_task_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create 3 new Task Orders
|
||||||
|
for i in range(3):
|
||||||
|
TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
|
|
||||||
|
dispatch_send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that send_with_attachment was called once for each task order
|
||||||
|
assert mock.call_count == 3
|
||||||
|
mock.reset_mock()
|
||||||
|
|
||||||
|
# Create new TO
|
||||||
|
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
|
assert not task_order.pdf_last_sent_at
|
||||||
|
|
||||||
|
dispatch_send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that send_with_attachment was called with correct kwargs
|
||||||
|
mock.assert_called_once_with(
|
||||||
|
recipients=[app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")],
|
||||||
|
subject=translate(
|
||||||
|
"email.task_order_sent.subject", {"to_number": task_order.number}
|
||||||
|
),
|
||||||
|
body=translate("email.task_order_sent.body", {"to_number": task_order.number}),
|
||||||
|
attachments=[
|
||||||
|
{
|
||||||
|
"name": task_order.pdf.object_name,
|
||||||
|
"maintype": "application",
|
||||||
|
"subtype": "pdf",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert task_order.pdf_last_sent_at
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispatch_send_task_order_files_send_failure(monkeypatch):
|
||||||
|
def _raise_smtp_exception(**kwargs):
|
||||||
|
raise SMTPException
|
||||||
|
|
||||||
|
monkeypatch.setattr("atst.jobs.send_mail", _raise_smtp_exception)
|
||||||
|
|
||||||
|
task_order = TaskOrderFactory.create(create_clins=[{"number": "0001"}])
|
||||||
|
dispatch_send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that pdf_last_sent_at has not been updated
|
||||||
|
assert not task_order.pdf_last_sent_at
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispatch_send_task_order_files_download_failure(monkeypatch):
|
||||||
|
mock = Mock()
|
||||||
|
monkeypatch.setattr("atst.jobs.send_mail", mock)
|
||||||
|
|
||||||
|
def _download_task_order(MockFileService, object_name):
|
||||||
|
raise AzureError("something went wrong")
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"atst.domain.csp.files.MockFileService.download_task_order",
|
||||||
|
_download_task_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
task_order = TaskOrderFactory.create(create_clins=[{"number": "0002"}])
|
||||||
|
dispatch_send_task_order_files.run()
|
||||||
|
|
||||||
|
# Check that pdf_last_sent_at has not been updated
|
||||||
|
assert not task_order.pdf_last_sent_at
|
||||||
|
@ -5,9 +5,12 @@ from unittest.mock import Mock
|
|||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from flask import template_rendered
|
from flask import template_rendered
|
||||||
|
import pendulum
|
||||||
|
|
||||||
from atst.utils.notification_sender import NotificationSender
|
from atst.utils.notification_sender import NotificationSender
|
||||||
|
|
||||||
|
import tests.factories as factories
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def captured_templates(app):
|
def captured_templates(app):
|
||||||
@ -62,3 +65,46 @@ def make_crl_list(x509_obj, x509_path):
|
|||||||
issuer = x509_obj.issuer.public_bytes(default_backend())
|
issuer = x509_obj.issuer.public_bytes(default_backend())
|
||||||
filename = os.path.basename(x509_path)
|
filename = os.path.basename(x509_path)
|
||||||
return [(filename, issuer.hex())]
|
return [(filename, issuer.hex())]
|
||||||
|
|
||||||
|
|
||||||
|
class EnvQueryTest:
|
||||||
|
@property
|
||||||
|
def NOW(self):
|
||||||
|
return pendulum.now()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def YESTERDAY(self):
|
||||||
|
return self.NOW.subtract(days=1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def TOMORROW(self):
|
||||||
|
return self.NOW.add(days=1)
|
||||||
|
|
||||||
|
def create_portfolio_with_clins(
|
||||||
|
self,
|
||||||
|
start_and_end_dates,
|
||||||
|
env_data=None,
|
||||||
|
app_data=None,
|
||||||
|
state_machine_status=None,
|
||||||
|
):
|
||||||
|
env_data = env_data or {}
|
||||||
|
app_data = app_data or {}
|
||||||
|
return factories.PortfolioFactory.create(
|
||||||
|
state=state_machine_status,
|
||||||
|
applications=[
|
||||||
|
{
|
||||||
|
"name": "Mos Eisley",
|
||||||
|
"description": "Where Han shot first",
|
||||||
|
"environments": [{"name": "thebar", **env_data}],
|
||||||
|
**app_data,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
task_orders=[
|
||||||
|
{
|
||||||
|
"create_clins": [
|
||||||
|
{"start_date": start_date, "end_date": end_date}
|
||||||
|
for (start_date, end_date) in start_and_end_dates
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@ -128,9 +128,6 @@ flash:
|
|||||||
message: There was an error processing the invitation for {user_name} from {application_name}
|
message: There was an error processing the invitation for {user_name} from {application_name}
|
||||||
resent:
|
resent:
|
||||||
message: "{email} has been sent an invitation to access this Application"
|
message: "{email} has been sent an invitation to access this Application"
|
||||||
revoked:
|
|
||||||
title: Application invitation revoked
|
|
||||||
message: You have successfully revoked the invite for {user_name} from {application_name}
|
|
||||||
application_member:
|
application_member:
|
||||||
removed:
|
removed:
|
||||||
title: Team member removed from application
|
title: Team member removed from application
|
||||||
@ -166,6 +163,9 @@ flash:
|
|||||||
errors:
|
errors:
|
||||||
title: There were some errors
|
title: There were some errors
|
||||||
message: Please see below.
|
message: Please see below.
|
||||||
|
invite_revoked:
|
||||||
|
title: "{resource} invitation revoked"
|
||||||
|
message: "You have successfully revoked the invite for {user_name} from {resource_name}"
|
||||||
login_required_message: After you log in, you will be redirected to your destination page.
|
login_required_message: After you log in, you will be redirected to your destination page.
|
||||||
login_required_title: Log in required
|
login_required_title: Log in required
|
||||||
logged_out:
|
logged_out:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user