Merge branch 'staging' into environment-role-creation
This commit is contained in:
commit
872500099e
@ -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 ###
|
@ -1,7 +1,7 @@
|
|||||||
"""change to environment_roles.cloud_Id
|
"""change to environment_roles.cloud_Id
|
||||||
|
|
||||||
Revision ID: 418b52c1cedf
|
Revision ID: 418b52c1cedf
|
||||||
Revises: 17da2a475429
|
Revises: 0039308c6351
|
||||||
Create Date: 2020-02-05 13:40:37.870183
|
Create Date: 2020-02-05 13:40:37.870183
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -11,7 +11,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '418b52c1cedf' # pragma: allowlist secret
|
revision = '418b52c1cedf' # pragma: allowlist secret
|
||||||
down_revision = '17da2a475429' # pragma: allowlist secret
|
down_revision = '0039308c6351' # pragma: allowlist secret
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
@ -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,
|
||||||
@ -55,7 +62,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
|
||||||
@ -122,11 +128,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()
|
||||||
@ -135,67 +145,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(
|
||||||
def create_environment(self, auth_credentials: Dict, user, environment):
|
f"Could not GET secret in Azure keyvault for key {secret_key}.",
|
||||||
# since this operation would only occur within a tenant, should we source the tenant
|
exc.message,
|
||||||
# via lookup from environment once we've created the portfolio csp data schema
|
|
||||||
# something like this:
|
|
||||||
# environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None)
|
|
||||||
# though we'd probably source the whole credentials for these calls from the portfolio csp
|
|
||||||
# data, as it would have to be where we store the creds for the at-at user within the portfolio tenant
|
|
||||||
# credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds())
|
|
||||||
credentials = self._get_credential_obj(self._root_creds)
|
|
||||||
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)
|
def create_environment(self, payload: EnvironmentCSPPayload):
|
||||||
|
creds = self._source_creds(payload.tenant_id)
|
||||||
|
credentials = self._get_credential_obj(
|
||||||
|
{
|
||||||
|
"client_id": creds.tenant_sp_client_id,
|
||||||
|
"secret_key": creds.tenant_sp_key,
|
||||||
|
"tenant_id": creds.tenant_id,
|
||||||
|
},
|
||||||
|
resource=self.sdk.cloud.endpoints.resource_manager,
|
||||||
|
)
|
||||||
|
|
||||||
def create_atat_admin_user(
|
response = self._create_management_group(
|
||||||
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)
|
||||||
@ -804,17 +782,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
|
||||||
@ -941,7 +908,8 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
"Could not resolve graph token for tenant admin"
|
"Could not resolve graph token for tenant admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.management_group_id}/providers/Microsoft.Authorization/roleDefinitions/{self.roles[payload.role]}"
|
role_guid = self.roles[payload.role]
|
||||||
|
role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.management_group_id}/providers/Microsoft.Authorization/roleDefinitions/{role_guid}"
|
||||||
|
|
||||||
request_body = {
|
request_body = {
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -1109,3 +1077,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: # pragma: no cover
|
|||||||
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: # pragma: no cover
|
|||||||
"""
|
"""
|
||||||
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,)
|
||||||
|
)
|
||||||
|
@ -359,6 +359,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]
|
||||||
@ -515,3 +523,34 @@ class UserRoleCSPPayload(BaseCSPPayload):
|
|||||||
|
|
||||||
class UserRoleCSPResult(AliasModel):
|
class UserRoleCSPResult(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")
|
||||||
|
@ -110,9 +110,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.cloud_id and not environment_role.environment.is_pending:
|
if environment_role.cloud_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.cloud_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()
|
|
||||||
|
86
atst/jobs.py
86
atst/jobs.py
@ -1,24 +1,23 @@
|
|||||||
from flask import current_app as app
|
|
||||||
import pendulum
|
import pendulum
|
||||||
|
from flask import current_app as app
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.queue import celery
|
|
||||||
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.environments import Environments
|
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
|
||||||
from atst.domain.portfolios import Portfolios
|
|
||||||
from atst.domain.application_roles import ApplicationRoles
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
from atst.models.utils import claim_for_update, claim_many_for_update
|
from atst.domain.applications import Applications
|
||||||
from atst.models import CSPRole
|
from atst.domain.csp.cloud import CloudProviderInterface
|
||||||
from atst.utils.localization import translate
|
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||||
from atst.domain.csp.cloud.models import (
|
from atst.domain.csp.cloud.models import (
|
||||||
ApplicationCSPPayload,
|
ApplicationCSPPayload,
|
||||||
|
EnvironmentCSPPayload,
|
||||||
UserCSPPayload,
|
UserCSPPayload,
|
||||||
UserRoleCSPPayload,
|
UserRoleCSPPayload,
|
||||||
)
|
)
|
||||||
|
from atst.domain.environments import Environments
|
||||||
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
|
from atst.domain.portfolios import Portfolios
|
||||||
|
from atst.models import CSPRole, JobFailure
|
||||||
|
from atst.models.utils import claim_for_update, claim_many_for_update
|
||||||
|
from atst.queue import celery
|
||||||
|
|
||||||
|
|
||||||
class RecordFailure(celery.Task):
|
class RecordFailure(celery.Task):
|
||||||
@ -115,34 +114,19 @@ 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?
|
env_result = csp.create_environment(payload)
|
||||||
# if using global creds, do we need to log what user authorized action?
|
environment.cloud_id = env_result.id
|
||||||
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.add(environment)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
body = render_email(
|
|
||||||
"emails/application/environment_ready.txt", {"environment": environment}
|
|
||||||
)
|
|
||||||
app.mailer.send(
|
|
||||||
[environment.creator.email], translate("email.environment_ready"), body
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def do_create_environment_role(csp: CloudProviderInterface, environment_role_id=None):
|
def do_create_environment_role(csp: CloudProviderInterface, environment_role_id=None):
|
||||||
env_role = EnvironmentRoles.get_by_id(environment_role_id)
|
env_role = EnvironmentRoles.get_by_id(environment_role_id)
|
||||||
@ -164,7 +148,6 @@ def do_create_environment_role(csp: CloudProviderInterface, environment_role_id=
|
|||||||
elif env_role.role == CSPRole.CONTRIBUTOR:
|
elif env_role.role == CSPRole.CONTRIBUTOR:
|
||||||
role = UserRoleCSPPayload.Roles.contributor
|
role = UserRoleCSPPayload.Roles.contributor
|
||||||
|
|
||||||
# resolve role
|
|
||||||
payload = UserRoleCSPPayload(
|
payload = UserRoleCSPPayload(
|
||||||
tenant_id=csp_details.get("tenant_id"),
|
tenant_id=csp_details.get("tenant_id"),
|
||||||
management_group_id=env.cloud_id,
|
management_group_id=env.cloud_id,
|
||||||
@ -179,20 +162,6 @@ def do_create_environment_role(csp: CloudProviderInterface, environment_role_id=
|
|||||||
# TODO: should send notification email to the user, maybe with their portal login name
|
# TODO: should send notification email to the user, maybe with their portal login name
|
||||||
|
|
||||||
|
|
||||||
def do_create_atat_admin_user(csp: CloudProviderInterface, environment_id=None):
|
|
||||||
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.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def render_email(template_path, context):
|
def render_email(template_path, context):
|
||||||
return app.jinja_env.get_template(template_path).render(context)
|
return app.jinja_env.get_template(template_path).render(context)
|
||||||
|
|
||||||
@ -242,19 +211,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)
|
||||||
|
|
||||||
|
|
||||||
@ -282,11 +244,3 @@ def dispatch_create_environment(self):
|
|||||||
pendulum.now()
|
pendulum.now()
|
||||||
):
|
):
|
||||||
create_environment.delay(environment_id=environment_id)
|
create_environment.delay(environment_id=environment_id)
|
||||||
|
|
||||||
|
|
||||||
@celery.task(bind=True)
|
|
||||||
def dispatch_create_atat_admin_user(self):
|
|
||||||
for environment_id in Environments.get_environments_pending_atat_user_creation(
|
|
||||||
pendulum.now()
|
|
||||||
):
|
|
||||||
create_atat_admin_user.delay(environment_id=environment_id)
|
|
||||||
|
@ -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,17 +61,6 @@ 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
|
|
||||||
def is_pending(self):
|
|
||||||
return self.provisioning_status == self.ProvisioningStatus.PENDING
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
||||||
self.name,
|
self.name,
|
||||||
@ -91,11 +73,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)
|
||||||
|
@ -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,
|
||||||
|
@ -39,7 +39,6 @@ def get_environments_obj_for_app(application):
|
|||||||
{
|
{
|
||||||
"id": env.id,
|
"id": env.id,
|
||||||
"name": env.name,
|
"name": env.name,
|
||||||
"pending": env.is_pending,
|
|
||||||
"edit_form": EditEnvironmentForm(obj=env),
|
"edit_form": EditEnvironmentForm(obj=env),
|
||||||
"member_count": len(env.roles),
|
"member_count": len(env.roles),
|
||||||
"members": sorted(
|
"members": sorted(
|
||||||
@ -467,9 +466,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",
|
||||||
|
@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
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: ['!{', '}'],
|
||||||
|
|
||||||
|
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: $max-page-width;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: unset;
|
||||||
|
border: none;
|
||||||
max-width: 475px;
|
max-width: 475px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
.panel {
|
|
||||||
&__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;
|
||||||
|
@ -19,6 +19,7 @@ $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;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* USWDS Variables
|
* USWDS Variables
|
||||||
|
@ -32,22 +32,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-group-footer {
|
.action-group-footer {
|
||||||
|
padding-top: $gap;
|
||||||
|
padding-bottom: $gap;
|
||||||
|
padding-right: $gap * 4;
|
||||||
|
position: fixed;
|
||||||
|
bottom: $footer-height;
|
||||||
|
left: 0;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid $color-gray-lighter;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.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;
|
@extend .action-group;
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: $large-spacing;
|
||||||
|
max-width: $max-panel-width;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
margin-top: 0;
|
}
|
||||||
margin-bottom: 0;
|
|
||||||
padding-top: $gap;
|
|
||||||
padding-bottom: $gap;
|
|
||||||
|
|
||||||
position: fixed;
|
|
||||||
bottom: $footer-height;
|
|
||||||
background: white;
|
|
||||||
right: 0;
|
|
||||||
padding-right: $gap * 4;
|
|
||||||
border-top: 1px solid $color-gray-lighter;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
@ -39,14 +39,18 @@
|
|||||||
</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 %}
|
||||||
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</base-form>
|
</base-form>
|
||||||
|
|
||||||
|
@ -61,7 +61,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
||||||
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -71,7 +74,8 @@
|
|||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</application-environments>
|
</application-environments>
|
||||||
|
|
||||||
|
@ -25,7 +25,10 @@
|
|||||||
action_update="applications.update_new_application_step_3") }}
|
action_update="applications.update_new_application_step_3") }}
|
||||||
|
|
||||||
|
|
||||||
<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">
|
||||||
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
|
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
|
||||||
{{ "portfolios.applications.new.step_3_button_text" | translate }}
|
{{ "portfolios.applications.new.step_3_button_text" | translate }}
|
||||||
</a>
|
</a>
|
||||||
@ -35,6 +38,7 @@
|
|||||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||||
{{ "common.cancel" | translate }}
|
{{ "common.cancel" | translate }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="ccpo-panel-container">
|
||||||
<base-form inline-template>
|
<base-form inline-template>
|
||||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.submit_new_user') }}" method="POST">
|
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.submit_new_user') }}" method="POST">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
@ -21,4 +22,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</base-form>
|
</base-form>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{% from "components/text_input.html" import TextInput %}
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="ccpo-panel-container">
|
||||||
{% if new_user %}
|
{% if new_user %}
|
||||||
<h3>{{ 'ccpo.form.confirm_user_title' | translate }}</h3>
|
<h3>{{ 'ccpo.form.confirm_user_title' | translate }}</h3>
|
||||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.confirm_new_user') }}" method="POST">
|
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.confirm_new_user') }}" method="POST">
|
||||||
@ -30,4 +31,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
{% from "components/modal.html" import Modal %}
|
{% from "components/modal.html" import Modal %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="ccpo-panel-container">
|
||||||
<div class='col'>
|
<div class='col'>
|
||||||
<div class="h2">
|
<div class="h2">
|
||||||
{{ "ccpo.users_title" | translate }}
|
{{ "ccpo.users_title" | translate }}
|
||||||
@ -80,4 +81,5 @@
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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,7 +10,7 @@
|
|||||||
<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' %}
|
||||||
@ -34,5 +34,6 @@
|
|||||||
{% assets "js_all" %}
|
{% assets "js_all" %}
|
||||||
<script src="{{ ASSET_URL }}"></script>
|
<script src="{{ ASSET_URL }}"></script>
|
||||||
{% endassets %}
|
{% endassets %}
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
<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">
|
||||||
@ -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
|
||||||
|
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 %}
|
||||||
{{ SaveButton(text=('portfolios.new.save' | translate), form="portfolio-create", element="input") }}
|
{{ SaveButton(text=('portfolios.new.save' | translate), form="portfolio-create", element="input") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<a class="usa-button usa-button-secondary" href="{{ url_for('atst.home') }}">
|
<a class="usa-button usa-button-secondary" href="{{ url_for('atst.home') }}">
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</base-form>
|
</base-form>
|
||||||
|
@ -31,7 +31,10 @@
|
|||||||
<div class="task-order">
|
<div class="task-order">
|
||||||
{% 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,7 +52,10 @@ from atst.domain.csp.cloud.models import (
|
|||||||
TenantPrincipalCSPResult,
|
TenantPrincipalCSPResult,
|
||||||
TenantPrincipalOwnershipCSPPayload,
|
TenantPrincipalOwnershipCSPPayload,
|
||||||
TenantPrincipalOwnershipCSPResult,
|
TenantPrincipalOwnershipCSPResult,
|
||||||
|
UserCSPPayload,
|
||||||
|
UserRoleCSPPayload,
|
||||||
)
|
)
|
||||||
|
from atst.domain.csp.cloud.exceptions import UserProvisioningException
|
||||||
|
|
||||||
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 +68,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 +110,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 +161,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 +593,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 +677,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 +767,224 @@ 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"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user_role(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.put.return_value = mock_result_create
|
||||||
|
|
||||||
|
payload = UserRoleCSPPayload(
|
||||||
|
tenant_id=uuid4().hex,
|
||||||
|
user_object_id=str(uuid4()),
|
||||||
|
management_group_id=str(uuid4()),
|
||||||
|
role="owner",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = mock_azure.create_user_role(payload)
|
||||||
|
|
||||||
|
assert result.id == "id"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user_role_failure(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 = False
|
||||||
|
mock_azure.sdk.requests.put.return_value = mock_result_create
|
||||||
|
|
||||||
|
payload = UserRoleCSPPayload(
|
||||||
|
tenant_id=uuid4().hex,
|
||||||
|
user_object_id=str(uuid4()),
|
||||||
|
management_group_id=str(uuid4()),
|
||||||
|
role="owner",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(UserProvisioningException):
|
||||||
|
mock_azure.create_user_role(payload)
|
||||||
|
@ -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.cloud_id
|
assert not env_role1.cloud_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):
|
||||||
|
def test_finds_unstarted(self):
|
||||||
for x in range(5):
|
for x in range(5):
|
||||||
portfolio = PortfolioFactory.create()
|
|
||||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
|
||||||
if x == 2:
|
if x == 2:
|
||||||
sm.state = FSMStates.COMPLETED
|
state = "COMPLETED"
|
||||||
assert len(Portfolios.get_portfolios_pending_provisioning()) == 4
|
else:
|
||||||
|
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
|
||||||
|
@ -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},
|
||||||
|
@ -14,7 +14,6 @@ from atst.jobs import (
|
|||||||
dispatch_create_application,
|
dispatch_create_application,
|
||||||
dispatch_create_user,
|
dispatch_create_user,
|
||||||
dispatch_create_environment_role,
|
dispatch_create_environment_role,
|
||||||
dispatch_create_atat_admin_user,
|
|
||||||
dispatch_provision_portfolio,
|
dispatch_provision_portfolio,
|
||||||
create_environment,
|
create_environment,
|
||||||
do_create_user,
|
do_create_user,
|
||||||
@ -22,7 +21,6 @@ from atst.jobs import (
|
|||||||
do_create_environment,
|
do_create_environment,
|
||||||
do_create_environment_role,
|
do_create_environment_role,
|
||||||
do_create_application,
|
do_create_application,
|
||||||
do_create_atat_admin_user,
|
|
||||||
)
|
)
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
EnvironmentFactory,
|
EnvironmentFactory,
|
||||||
@ -97,6 +95,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)
|
||||||
|
|
||||||
@ -152,19 +154,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": [
|
||||||
@ -230,36 +224,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": [
|
||||||
@ -289,9 +256,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)
|
||||||
|
@ -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