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
|
||||
|
||||
Revision ID: 418b52c1cedf
|
||||
Revises: 17da2a475429
|
||||
Revises: 0039308c6351
|
||||
Create Date: 2020-02-05 13:40:37.870183
|
||||
|
||||
"""
|
||||
@ -11,7 +11,7 @@ import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '418b52c1cedf' # pragma: allowlist secret
|
||||
down_revision = '17da2a475429' # pragma: allowlist secret
|
||||
down_revision = '0039308c6351' # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
@ -6,12 +6,12 @@ from uuid import uuid4
|
||||
from atst.utils import sha256_hex
|
||||
|
||||
from .cloud_provider_interface import CloudProviderInterface
|
||||
from .exceptions import AuthenticationException, UserProvisioningException
|
||||
from .exceptions import (
|
||||
AuthenticationException,
|
||||
SecretException,
|
||||
UserProvisioningException,
|
||||
)
|
||||
from .models import (
|
||||
SubscriptionCreationCSPPayload,
|
||||
SubscriptionCreationCSPResult,
|
||||
SubscriptionVerificationCSPPayload,
|
||||
SuscriptionVerificationCSPResult,
|
||||
AdminRoleDefinitionCSPPayload,
|
||||
AdminRoleDefinitionCSPResult,
|
||||
ApplicationCSPPayload,
|
||||
@ -24,14 +24,21 @@ from .models import (
|
||||
BillingProfileTenantAccessCSPResult,
|
||||
BillingProfileVerificationCSPPayload,
|
||||
BillingProfileVerificationCSPResult,
|
||||
CostManagementQueryCSPResult,
|
||||
EnvironmentCSPPayload,
|
||||
EnvironmentCSPResult,
|
||||
KeyVaultCredentials,
|
||||
ManagementGroupCSPResponse,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
ProductPurchaseCSPPayload,
|
||||
ProductPurchaseCSPResult,
|
||||
ProductPurchaseVerificationCSPPayload,
|
||||
ProductPurchaseVerificationCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
ReportingCSPPayload,
|
||||
SubscriptionCreationCSPPayload,
|
||||
SubscriptionCreationCSPResult,
|
||||
SubscriptionVerificationCSPPayload,
|
||||
SuscriptionVerificationCSPResult,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
@ -55,7 +62,6 @@ from .models import (
|
||||
)
|
||||
from .policy import AzurePolicyManager
|
||||
|
||||
|
||||
# This needs to be a fully pathed role definition identifier, not just a UUID
|
||||
# TODO: Extract these from sdk msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
AZURE_SKU_ID = "0001" # probably a static sku specific to ATAT/JEDI
|
||||
@ -122,11 +128,15 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
)
|
||||
try:
|
||||
return secret_client.set_secret(secret_key, secret_value)
|
||||
except self.exceptions.HttpResponseError:
|
||||
except self.sdk.exceptions.HttpResponseError as exc:
|
||||
app.logger.error(
|
||||
f"Could not SET secret in Azure keyvault for key {secret_key}.",
|
||||
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):
|
||||
credential = self._get_client_secret_credential_obj()
|
||||
@ -135,67 +145,35 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
)
|
||||
try:
|
||||
return secret_client.get_secret(secret_key).value
|
||||
except self.exceptions.HttpResponseError:
|
||||
except self.sdk.exceptions.HttpResponseError:
|
||||
app.logger.error(
|
||||
f"Could not GET secret in Azure keyvault for key {secret_key}.",
|
||||
exc_info=1,
|
||||
)
|
||||
raise SecretException(
|
||||
f"Could not GET secret in Azure keyvault for key {secret_key}.",
|
||||
exc.message,
|
||||
)
|
||||
|
||||
def create_environment(self, auth_credentials: Dict, user, environment):
|
||||
# since this operation would only occur within a tenant, should we source the tenant
|
||||
# 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,
|
||||
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,
|
||||
)
|
||||
|
||||
return ManagementGroupCSPResponse(**management_group)
|
||||
|
||||
def create_atat_admin_user(
|
||||
self, auth_credentials: Dict, csp_environment_id: str
|
||||
) -> Dict:
|
||||
root_creds = self._root_creds
|
||||
credentials = self._get_credential_obj(root_creds)
|
||||
|
||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
||||
subscription = sub_client.subscriptions.get(csp_environment_id)
|
||||
|
||||
managment_principal = self._get_management_service_principal()
|
||||
|
||||
auth_client = self.sdk.authorization.AuthorizationManagementClient(
|
||||
response = self._create_management_group(
|
||||
credentials,
|
||||
# TODO: Determine which subscription this needs to point at
|
||||
# Once we're in a multi-sub environment
|
||||
subscription.id,
|
||||
payload.management_group_name,
|
||||
payload.display_name,
|
||||
payload.parent_id,
|
||||
)
|
||||
|
||||
# Create role assignment for
|
||||
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,
|
||||
}
|
||||
return EnvironmentCSPResult(**response)
|
||||
|
||||
def create_application(self, payload: ApplicationCSPPayload):
|
||||
creds = self._source_creds(payload.tenant_id)
|
||||
@ -804,17 +782,6 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
|
||||
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):
|
||||
# we really should be using graph.microsoft.com, but i'm getting
|
||||
# "expired token" errors for that
|
||||
@ -941,7 +908,8 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"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 = {
|
||||
"properties": {
|
||||
@ -1109,3 +1077,41 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
hashed = sha256_hex(tenant_id)
|
||||
raw_creds = self.get_secret(hashed)
|
||||
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:
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_environment(self, auth_credentials: Dict, user, environment) -> str:
|
||||
def create_environment(self, payload):
|
||||
"""Create a new environment in the CSP.
|
||||
|
||||
Arguments:
|
||||
@ -31,33 +31,6 @@ class CloudProviderInterface: # pragma: no cover
|
||||
"""
|
||||
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(
|
||||
self, auth_credentials: Dict, user_info, csp_role_id: str
|
||||
) -> str:
|
||||
|
@ -118,3 +118,17 @@ class BaselineProvisionException(GeneralCSPException):
|
||||
return "Could not complete baseline provisioning for environment ({}): {}".format(
|
||||
self.env_identifier, self.reason
|
||||
)
|
||||
|
||||
|
||||
class SecretException(GeneralCSPException):
|
||||
"""A problem occurred with setting or getting secrets"""
|
||||
|
||||
def __init__(self, tenant_id, reason):
|
||||
self.tenant_id = tenant_id
|
||||
self.reason = reason
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return "Could not get or set secret for ({}): {}".format(
|
||||
self.tenant_id, self.reason
|
||||
)
|
||||
|
@ -4,9 +4,7 @@ from .cloud_provider_interface import CloudProviderInterface
|
||||
from .exceptions import (
|
||||
AuthenticationException,
|
||||
AuthorizationException,
|
||||
BaselineProvisionException,
|
||||
ConnectionException,
|
||||
EnvironmentCreationException,
|
||||
GeneralCSPException,
|
||||
UnknownServerException,
|
||||
UserProvisioningException,
|
||||
@ -25,16 +23,21 @@ from .models import (
|
||||
BillingProfileTenantAccessCSPResult,
|
||||
BillingProfileVerificationCSPPayload,
|
||||
BillingProfileVerificationCSPResult,
|
||||
CostManagementQueryCSPResult,
|
||||
CostManagementQueryProperties,
|
||||
ProductPurchaseCSPPayload,
|
||||
ProductPurchaseCSPResult,
|
||||
ProductPurchaseVerificationCSPPayload,
|
||||
ProductPurchaseVerificationCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
ReportingCSPPayload,
|
||||
SubscriptionCreationCSPPayload,
|
||||
SubscriptionCreationCSPResult,
|
||||
SubscriptionVerificationCSPPayload,
|
||||
SuscriptionVerificationCSPResult,
|
||||
EnvironmentCSPPayload,
|
||||
EnvironmentCSPResult,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
@ -91,34 +94,6 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
def get_secret(self, secret_key: str, default=dict()):
|
||||
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):
|
||||
return self.create_subscription_creation(payload)
|
||||
|
||||
@ -142,23 +117,6 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
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):
|
||||
"""
|
||||
payload is an instance of TenantCSPPayload data class
|
||||
@ -477,6 +435,13 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
|
||||
)
|
||||
|
||||
def create_environment(self, payload: EnvironmentCSPPayload):
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||
|
||||
return EnvironmentCSPResult(
|
||||
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
|
||||
)
|
||||
|
||||
def create_user(self, payload: UserCSPPayload):
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
|
||||
|
||||
@ -487,3 +452,25 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
|
||||
def update_tenant_creds(self, tenant_id, secret):
|
||||
return secret
|
||||
|
||||
def get_reporting_data(self, payload: ReportingCSPPayload):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
object_id = str(uuid4())
|
||||
|
||||
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
|
||||
|
||||
|
||||
class EnvironmentCSPPayload(ManagementGroupCSPPayload):
|
||||
pass
|
||||
|
||||
|
||||
class EnvironmentCSPResult(ManagementGroupCSPResponse):
|
||||
pass
|
||||
|
||||
|
||||
class KeyVaultCredentials(BaseModel):
|
||||
root_sp_client_id: Optional[str]
|
||||
root_sp_key: Optional[str]
|
||||
@ -515,3 +523,34 @@ class UserRoleCSPPayload(BaseCSPPayload):
|
||||
|
||||
class UserRoleCSPResult(AliasModel):
|
||||
id: str
|
||||
|
||||
|
||||
class QueryColumn(AliasModel):
|
||||
name: str
|
||||
type: str
|
||||
|
||||
|
||||
class CostManagementQueryProperties(AliasModel):
|
||||
columns: List[QueryColumn]
|
||||
rows: List[Optional[list]]
|
||||
|
||||
|
||||
class CostManagementQueryCSPResult(AliasModel):
|
||||
name: str
|
||||
properties: CostManagementQueryProperties
|
||||
|
||||
|
||||
class ReportingCSPPayload(BaseCSPPayload):
|
||||
invoice_section_id: str
|
||||
from_date: str
|
||||
to_date: str
|
||||
|
||||
@root_validator(pre=True)
|
||||
def extract_invoice_section(cls, values):
|
||||
try:
|
||||
values["invoice_section_id"] = values["billing_profile_properties"][
|
||||
"invoice_sections"
|
||||
][0]["invoice_section_id"]
|
||||
return values
|
||||
except (KeyError, IndexError):
|
||||
raise ValueError("Invoice section ID not present in payload")
|
||||
|
@ -110,9 +110,11 @@ class EnvironmentRoles(object):
|
||||
def disable(cls, environment_role_id):
|
||||
environment_role = EnvironmentRoles.get_by_id(environment_role_id)
|
||||
|
||||
if environment_role.cloud_id and not environment_role.environment.is_pending:
|
||||
credentials = environment_role.environment.csp_credentials
|
||||
app.csp.cloud.disable_user(credentials, environment_role.cloud_id)
|
||||
if environment_role.cloud_id and not environment_role.environment.cloud_id:
|
||||
tenant_id = environment_role.environment.application.portfolio.csp_data.get(
|
||||
"tenant_id"
|
||||
)
|
||||
app.csp.cloud.disable_user(tenant_id, environment_role.csp_user_id)
|
||||
|
||||
environment_role.status = EnvironmentRole.Status.DISABLED
|
||||
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`.
|
||||
"""
|
||||
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]
|
||||
|
||||
@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,
|
||||
PortfolioRole,
|
||||
PortfolioRoleStatus,
|
||||
TaskOrder,
|
||||
CLIN,
|
||||
)
|
||||
|
||||
from .query import PortfoliosQuery, PortfolioStateMachinesQuery
|
||||
@ -144,7 +146,7 @@ class Portfolios(object):
|
||||
return db.session.query(Portfolio.id)
|
||||
|
||||
@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:
|
||||
not started yet,
|
||||
@ -153,22 +155,18 @@ class Portfolios(object):
|
||||
"""
|
||||
|
||||
results = (
|
||||
cls.base_provision_query()
|
||||
db.session.query(Portfolio.id)
|
||||
.join(PortfolioStateMachine)
|
||||
.join(TaskOrder)
|
||||
.join(CLIN)
|
||||
.filter(Portfolio.deleted == False)
|
||||
.filter(CLIN.start_date <= now)
|
||||
.filter(CLIN.end_date > now)
|
||||
.filter(
|
||||
or_(
|
||||
PortfolioStateMachine.state == FSMStates.UNSTARTED,
|
||||
PortfolioStateMachine.state == FSMStates.FAILED,
|
||||
PortfolioStateMachine.state == FSMStates.TENANT_FAILED,
|
||||
PortfolioStateMachine.state.like("%CREATED"),
|
||||
)
|
||||
)
|
||||
)
|
||||
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
|
||||
from flask import current_app as app
|
||||
|
||||
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.models.utils import claim_for_update, claim_many_for_update
|
||||
from atst.models import CSPRole
|
||||
from atst.utils.localization import translate
|
||||
from atst.domain.applications import Applications
|
||||
from atst.domain.csp.cloud import CloudProviderInterface
|
||||
from atst.domain.csp.cloud.exceptions import GeneralCSPException
|
||||
from atst.domain.csp.cloud.models import (
|
||||
ApplicationCSPPayload,
|
||||
EnvironmentCSPPayload,
|
||||
UserCSPPayload,
|
||||
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):
|
||||
@ -115,34 +114,19 @@ def do_create_environment(csp: CloudProviderInterface, environment_id=None):
|
||||
with claim_for_update(environment) as environment:
|
||||
|
||||
if environment.cloud_id is not None:
|
||||
# TODO: Return value for this?
|
||||
return
|
||||
|
||||
user = environment.creator
|
||||
|
||||
# we'll need to do some checking in this job for cases where it's retrying
|
||||
# when a failure occured after some successful steps
|
||||
# (e.g. if environment.cloud_id is not None, then we can skip first step)
|
||||
|
||||
# credentials either from a given user or pulled from config?
|
||||
# if using global creds, do we need to log what user authorized action?
|
||||
atat_root_creds = csp.root_creds()
|
||||
|
||||
# user is needed because baseline root account in the environment will
|
||||
# be assigned to the requesting user, open question how to handle duplicate
|
||||
# email addresses across new environments
|
||||
csp_environment_id = csp.create_environment(atat_root_creds, user, environment)
|
||||
environment.cloud_id = csp_environment_id
|
||||
csp_details = environment.application.portfolio.csp_data
|
||||
parent_id = environment.application.cloud_id
|
||||
tenant_id = csp_details.get("tenant_id")
|
||||
payload = EnvironmentCSPPayload(
|
||||
tenant_id=tenant_id, display_name=environment.name, parent_id=parent_id
|
||||
)
|
||||
env_result = csp.create_environment(payload)
|
||||
environment.cloud_id = env_result.id
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
|
||||
body = render_email(
|
||||
"emails/application/environment_ready.txt", {"environment": environment}
|
||||
)
|
||||
app.mailer.send(
|
||||
[environment.creator.email], translate("email.environment_ready"), body
|
||||
)
|
||||
|
||||
|
||||
def do_create_environment_role(csp: CloudProviderInterface, environment_role_id=None):
|
||||
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:
|
||||
role = UserRoleCSPPayload.Roles.contributor
|
||||
|
||||
# resolve role
|
||||
payload = UserRoleCSPPayload(
|
||||
tenant_id=csp_details.get("tenant_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
|
||||
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
@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)
|
||||
def dispatch_provision_portfolio(self):
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@ -282,11 +244,3 @@ def dispatch_create_environment(self):
|
||||
pendulum.now()
|
||||
):
|
||||
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.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.types as types
|
||||
from atst.models.base import Base
|
||||
|
||||
|
||||
class Environment(
|
||||
@ -30,7 +28,6 @@ class Environment(
|
||||
creator = relationship("User")
|
||||
|
||||
cloud_id = Column(String)
|
||||
root_user_info = Column(JSONB(none_as_null=True))
|
||||
|
||||
roles = relationship(
|
||||
"EnvironmentRole",
|
||||
@ -44,10 +41,6 @@ class Environment(
|
||||
),
|
||||
)
|
||||
|
||||
class ProvisioningStatus(Enum):
|
||||
PENDING = "pending"
|
||||
COMPLETED = "completed"
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
return {r.application_role.user for r in self.roles}
|
||||
@ -68,17 +61,6 @@ class Environment(
|
||||
def portfolio_id(self):
|
||||
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):
|
||||
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
||||
self.name,
|
||||
@ -91,11 +73,3 @@ class Environment(
|
||||
@property
|
||||
def history(self):
|
||||
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())
|
||||
print(exc.json())
|
||||
app.logger.info(payload_data)
|
||||
# TODO: Ensure that failing the stage does not preclude a Celery retry
|
||||
self.fail_stage(stage)
|
||||
# TODO: catch and handle general CSP exception here
|
||||
except (ConnectionException, UnknownServerException) as exc:
|
||||
app.logger.error(
|
||||
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.finish_stage(stage)
|
||||
|
@ -19,10 +19,6 @@ def update_celery(celery, app):
|
||||
"task": "atst.jobs.dispatch_create_environment",
|
||||
"schedule": 60,
|
||||
},
|
||||
"beat-dispatch_create_atat_admin_user": {
|
||||
"task": "atst.jobs.dispatch_create_atat_admin_user",
|
||||
"schedule": 60,
|
||||
},
|
||||
"beat-dispatch_create_user": {
|
||||
"task": "atst.jobs.dispatch_create_user",
|
||||
"schedule": 60,
|
||||
|
@ -39,7 +39,6 @@ def get_environments_obj_for_app(application):
|
||||
{
|
||||
"id": env.id,
|
||||
"name": env.name,
|
||||
"pending": env.is_pending,
|
||||
"edit_form": EditEnvironmentForm(obj=env),
|
||||
"member_count": len(env.roles),
|
||||
"members": sorted(
|
||||
@ -467,9 +466,10 @@ def revoke_invite(application_id, application_role_id):
|
||||
if invite.is_pending:
|
||||
ApplicationInvitations.revoke(invite.token)
|
||||
flash(
|
||||
"application_invite_revoked",
|
||||
"invite_revoked",
|
||||
resource="Application",
|
||||
user_name=app_role.user_name,
|
||||
application_name=g.application.name,
|
||||
resource_name=g.application.name,
|
||||
)
|
||||
else:
|
||||
flash(
|
||||
|
@ -37,8 +37,14 @@ def accept_invitation(portfolio_token):
|
||||
)
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="revoke invitation")
|
||||
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(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
|
@ -33,11 +33,6 @@ MESSAGES = {
|
||||
"message": "flash.application_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_invite_revoked": {
|
||||
"title": "flash.application_invite.revoked.title",
|
||||
"message": "flash.application_invite.revoked.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_member_removed": {
|
||||
"title": "flash.application_member.removed.title",
|
||||
"message": "flash.application_member.removed.message",
|
||||
@ -103,6 +98,11 @@ MESSAGES = {
|
||||
"message": None,
|
||||
"category": "warning",
|
||||
},
|
||||
"invite_revoked": {
|
||||
"title": "flash.invite_revoked.title",
|
||||
"message": "flash.invite_revoked.message",
|
||||
"category": "success",
|
||||
},
|
||||
"logged_out": {
|
||||
"title": "flash.logged_out.title",
|
||||
"message": "flash.logged_out.message",
|
||||
|
@ -1,30 +1,21 @@
|
||||
import ExpandSidenavMixin from '../mixins/expand_sidenav'
|
||||
import ToggleMixin from '../mixins/toggle'
|
||||
|
||||
const cookieName = 'expandSidenav'
|
||||
|
||||
export default {
|
||||
name: 'sidenav-toggler',
|
||||
|
||||
mixins: [ToggleMixin],
|
||||
mixins: [ExpandSidenavMixin, ToggleMixin],
|
||||
|
||||
props: {
|
||||
defaultVisible: {
|
||||
type: Boolean,
|
||||
default: function() {
|
||||
if (document.cookie.match(cookieName)) {
|
||||
return !!document.cookie.match(cookieName + ' *= *true')
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted: function() {
|
||||
this.$parent.$emit('sidenavToggle', this.isVisible)
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle: function(e) {
|
||||
e.preventDefault()
|
||||
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 PopDateRange from './components/pop_date_range'
|
||||
import ToggleMenu from './components/toggle_menu'
|
||||
import ExpandSidenav from './mixins/expand_sidenav'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use(VTooltip)
|
||||
|
||||
Vue.mixin(Modal)
|
||||
Vue.mixin(ExpandSidenav)
|
||||
|
||||
const app = new Vue({
|
||||
el: '#app-root',
|
||||
@ -67,6 +69,12 @@ const app = new Vue({
|
||||
ToggleMenu,
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
sidenavExpanded: this.defaultVisible,
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.$on('modalOpen', data => {
|
||||
if (data['isOpen']) {
|
||||
@ -105,6 +113,10 @@ const app = new Vue({
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.$on('sidenavToggle', data => {
|
||||
this.sidenavExpanded = data
|
||||
})
|
||||
},
|
||||
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/reports";
|
||||
@import "sections/task_order";
|
||||
@import "sections/ccpo";
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
.usa-alert {
|
||||
padding-bottom: 2.4rem;
|
||||
max-width: $max-panel-width;
|
||||
}
|
||||
|
||||
@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 {
|
||||
max-width: 475px;
|
||||
margin: auto;
|
||||
max-width: $max-page-width;
|
||||
|
||||
.panel {
|
||||
box-shadow: none;
|
||||
background-color: unset;
|
||||
border: none;
|
||||
max-width: 475px;
|
||||
margin: auto;
|
||||
|
||||
&__heading {
|
||||
text-align: center;
|
||||
padding: $gap 0;
|
||||
@ -15,17 +20,6 @@
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: $gap * 2;
|
||||
margin: 0;
|
||||
|
||||
hr {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
margin-bottom: $gap * 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -12,7 +12,7 @@
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
max-width: 1190px;
|
||||
max-width: $max-page-width;
|
||||
|
||||
a {
|
||||
color: $color-white;
|
||||
|
@ -19,6 +19,7 @@ $sidenav-collapsed-width: 10rem;
|
||||
$max-panel-width: 90rem;
|
||||
$home-pg-icon-width: 6rem;
|
||||
$large-spacing: 4rem;
|
||||
$max-page-width: $max-panel-width + $sidenav-expanded-width + $large-spacing;
|
||||
|
||||
/*
|
||||
* USWDS Variables
|
||||
|
@ -32,22 +32,35 @@
|
||||
}
|
||||
|
||||
.action-group-footer {
|
||||
@extend .action-group;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: $gap;
|
||||
padding-bottom: $gap;
|
||||
|
||||
padding-right: $gap * 4;
|
||||
position: fixed;
|
||||
bottom: $footer-height;
|
||||
left: 0;
|
||||
background: white;
|
||||
right: 0;
|
||||
padding-right: $gap * 4;
|
||||
border-top: 1px solid $color-gray-lighter;
|
||||
width: 100%;
|
||||
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;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: $large-spacing;
|
||||
max-width: $max-panel-width;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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>
|
||||
|
||||
|
||||
<span class="action-group-footer">
|
||||
{% block next_button %}
|
||||
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
||||
{% endblock %}
|
||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||
Cancel
|
||||
</a>
|
||||
</span>
|
||||
<div
|
||||
class="action-group-footer"
|
||||
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||
<div class="action-group-footer--container">
|
||||
{% block next_button %}
|
||||
{{ SaveButton(text=('portfolios.applications.new.step_1_button_text' | translate)) }}
|
||||
{% endblock %}
|
||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</base-form>
|
||||
|
||||
|
@ -58,20 +58,24 @@
|
||||
{{ Icon("plus") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="action-group-footer"
|
||||
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||
<div class="action-group-footer--container">
|
||||
{% block next_button %}
|
||||
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
||||
{% endblock %}
|
||||
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_1', application_id=application.id) }}">
|
||||
Previous
|
||||
</a>
|
||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="action-group-footer">
|
||||
{% block next_button %}
|
||||
{{ SaveButton(text=('portfolios.applications.new.step_2_button_text' | translate)) }}
|
||||
{% endblock %}
|
||||
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_1', application_id=application.id) }}">
|
||||
Previous
|
||||
</a>
|
||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||
Cancel
|
||||
</a>
|
||||
</span>
|
||||
</form>
|
||||
</application-environments>
|
||||
|
||||
|
@ -25,16 +25,20 @@
|
||||
action_update="applications.update_new_application_step_3") }}
|
||||
|
||||
|
||||
<span class="action-group-footer">
|
||||
<a class="usa-button" href="{{ url_for('applications.settings', application_id=application_id) }}">
|
||||
{{ "portfolios.applications.new.step_3_button_text" | translate }}
|
||||
</a>
|
||||
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_2', application_id=application.id) }}">
|
||||
{{ "common.previous" | translate }}
|
||||
</a>
|
||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||
{{ "common.cancel" | translate }}
|
||||
</a>
|
||||
</span>
|
||||
<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) }}">
|
||||
{{ "portfolios.applications.new.step_3_button_text" | translate }}
|
||||
</a>
|
||||
<a class="usa-button usa-button-secondary" href="{{ url_for('applications.view_new_application_step_2', application_id=application.id) }}">
|
||||
{{ "common.previous" | translate }}
|
||||
</a>
|
||||
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
|
||||
{{ "common.cancel" | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -4,21 +4,23 @@
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
|
||||
{% block content %}
|
||||
<base-form inline-template>
|
||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.submit_new_user') }}" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<h1>{{ "ccpo.form.add_user_title" | translate }}</h1>
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--two-thirds'>
|
||||
{{ TextInput(form.dod_id, validation='dodId', optional=False) }}
|
||||
</div>
|
||||
<div class="form-col form-col--third">
|
||||
<div class='action-group'>
|
||||
{{ SaveButton(text="common.next"|translate, element="input", additional_classes="action-group__action", form="add-ccpo-user-form") }}
|
||||
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">{{ "common.cancel" | translate }}</a>
|
||||
<div class="ccpo-panel-container">
|
||||
<base-form inline-template>
|
||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.submit_new_user') }}" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<h1>{{ "ccpo.form.add_user_title" | translate }}</h1>
|
||||
<div class='form-row'>
|
||||
<div class='form-col form-col--two-thirds'>
|
||||
{{ TextInput(form.dod_id, validation='dodId', optional=False) }}
|
||||
</div>
|
||||
<div class="form-col form-col--third">
|
||||
<div class='action-group'>
|
||||
{{ SaveButton(text="common.next"|translate, element="input", additional_classes="action-group__action", form="add-ccpo-user-form") }}
|
||||
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</base-form>
|
||||
</form>
|
||||
</base-form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -3,31 +3,33 @@
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
|
||||
{% block content %}
|
||||
{% if new_user %}
|
||||
<h3>{{ 'ccpo.form.confirm_user_title' | translate }}</h3>
|
||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.confirm_new_user') }}" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<input type="hidden" name="dod_id" value="{{ form.dod_id.data }}">
|
||||
<div>
|
||||
<p>
|
||||
{{ "ccpo.form.confirm_user_text" | translate }}
|
||||
</p>
|
||||
<p>
|
||||
{{ new_user.full_name }}
|
||||
</p>
|
||||
<p>
|
||||
{{ new_user.email }}
|
||||
</p>
|
||||
</div>
|
||||
<div class='action-group'>
|
||||
<input
|
||||
type='submit'
|
||||
class='action-group__action usa-button'
|
||||
value='{{ "ccpo.form.confirm_button" | translate }}'>
|
||||
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">
|
||||
{{ "common.cancel" | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="ccpo-panel-container">
|
||||
{% if new_user %}
|
||||
<h3>{{ 'ccpo.form.confirm_user_title' | translate }}</h3>
|
||||
<form id="add-ccpo-user-form" action="{{ url_for('ccpo.confirm_new_user') }}" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<input type="hidden" name="dod_id" value="{{ form.dod_id.data }}">
|
||||
<div>
|
||||
<p>
|
||||
{{ "ccpo.form.confirm_user_text" | translate }}
|
||||
</p>
|
||||
<p>
|
||||
{{ new_user.full_name }}
|
||||
</p>
|
||||
<p>
|
||||
{{ new_user.email }}
|
||||
</p>
|
||||
</div>
|
||||
<div class='action-group'>
|
||||
<input
|
||||
type='submit'
|
||||
class='action-group__action usa-button'
|
||||
value='{{ "ccpo.form.confirm_button" | translate }}'>
|
||||
<a class='action-group__action icon-link icon-link--default' href="{{ url_for('ccpo.users') }}">
|
||||
{{ "common.cancel" | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -6,78 +6,80 @@
|
||||
{% from "components/modal.html" import Modal %}
|
||||
|
||||
{% block content %}
|
||||
<div class='col'>
|
||||
<div class="h2">
|
||||
{{ "ccpo.users_title" | translate }}
|
||||
</div>
|
||||
<div class="ccpo-panel-container">
|
||||
<div class='col'>
|
||||
<div class="h2">
|
||||
{{ "ccpo.users_title" | translate }}
|
||||
</div>
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ "common.name" | translate }}</th>
|
||||
<th>{{ "common.email" | translate }}</th>
|
||||
<th>{{ "common.dod_id" | translate }}</th>
|
||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||
<th></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user, form in users_info %}
|
||||
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
||||
{% set disable_button_class = 'button-danger-outline' %}
|
||||
{% if user == g.current_user %}
|
||||
{% set disable_button_class = "usa-button-disabled" %}
|
||||
{% endif %}
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ user.full_name }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.dod_id }}</td>
|
||||
<th>{{ "common.name" | translate }}</th>
|
||||
<th>{{ "common.email" | translate }}</th>
|
||||
<th>{{ "common.dod_id" | translate }}</th>
|
||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||
<td>
|
||||
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ disable_button_class }}'>
|
||||
{{ "common.disable" | translate }}
|
||||
</a>
|
||||
</td>
|
||||
<th></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user, form in users_info %}
|
||||
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
||||
{% set disable_button_class = 'button-danger-outline' %}
|
||||
{% if user == g.current_user %}
|
||||
{% set disable_button_class = "usa-button-disabled" %}
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td>{{ user.full_name }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.dod_id }}</td>
|
||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||
<td>
|
||||
<a v-on:click="openModal('{{ modal_id }}')" class='usa-button {{ disable_button_class }}'>
|
||||
{{ "common.disable" | translate }}
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if user_can(permissions.CREATE_CCPO_USER) %}
|
||||
<a class="icon-link" href="{{ url_for('ccpo.add_new_user')}}">
|
||||
{{ "ccpo.add_user" | translate }} {{ Icon("plus") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||
{% for user, form in users_info %}
|
||||
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
||||
{% call Modal(name=modal_id) %}
|
||||
<h1>Disable CCPO User</h1>
|
||||
<hr>
|
||||
{{
|
||||
Alert(
|
||||
title=("components.modal.destructive_title" | translate),
|
||||
message=("ccpo.disable_user.alert_message" | translate({"user_name": user.full_name})),
|
||||
level="warning"
|
||||
)
|
||||
}}
|
||||
{{
|
||||
DeleteConfirmation(
|
||||
modal_id=modal_id,
|
||||
delete_text='Remove Access',
|
||||
delete_action=(url_for('ccpo.remove_access', user_id=user.id)),
|
||||
form=form,
|
||||
confirmation_text='remove'
|
||||
)
|
||||
}}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if user_can(permissions.CREATE_CCPO_USER) %}
|
||||
<a class="icon-link" href="{{ url_for('ccpo.add_new_user')}}">
|
||||
{{ "ccpo.add_user" | translate }} {{ Icon("plus") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user_can(permissions.DELETE_CCPO_USER) %}
|
||||
{% for user, form in users_info %}
|
||||
{% set modal_id = "disable_ccpo_user_{}".format(user.dod_id) %}
|
||||
{% call Modal(name=modal_id) %}
|
||||
<h1>Disable CCPO User</h1>
|
||||
<hr>
|
||||
{{
|
||||
Alert(
|
||||
title=("components.modal.destructive_title" | translate),
|
||||
message=("ccpo.disable_user.alert_message" | translate({"user_name": user.full_name})),
|
||||
level="warning"
|
||||
)
|
||||
}}
|
||||
{{
|
||||
DeleteConfirmation(
|
||||
modal_id=modal_id,
|
||||
delete_text='Remove Access',
|
||||
delete_action=(url_for('ccpo.remove_access', user_id=user.id)),
|
||||
form=form,
|
||||
confirmation_text='remove'
|
||||
)
|
||||
}}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -5,6 +5,7 @@
|
||||
{% block content %}
|
||||
|
||||
<main class="usa-section usa-content error-page">
|
||||
<div class="panel">
|
||||
<div class="panel__heading">
|
||||
{{ Icon('cloud', classes="icon--red icon--large")}}
|
||||
<hr>
|
||||
@ -17,6 +18,7 @@
|
||||
{%- endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -10,29 +10,30 @@
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }} " />
|
||||
</head>
|
||||
<body class="{% if g.modalOpen %} modalOpen{% endif %}">
|
||||
<div id='app-root'>
|
||||
{% block template_vars %}{% endblock %}
|
||||
|
||||
{% block template_vars %}{% endblock %}
|
||||
{% include 'components/usa_header.html' %}
|
||||
|
||||
{% include 'components/usa_header.html' %}
|
||||
{% include 'navigation/topbar.html' %}
|
||||
|
||||
{% include 'navigation/topbar.html' %}
|
||||
<div class='global-layout'>
|
||||
|
||||
<div class='global-layout'>
|
||||
<div class='global-panel-container'>
|
||||
{% block sidenav %}{% endblock %}
|
||||
|
||||
<div class='global-panel-container'>
|
||||
{% block sidenav %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
these are not the droids you are looking for
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
these are not the droids you are looking for
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
{% block modal %}{% endblock %}
|
||||
{% assets "js_all" %}
|
||||
<script src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
{% block modal %}{% endblock %}
|
||||
{% assets "js_all" %}
|
||||
<script src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -15,6 +15,7 @@
|
||||
<p>{{ "portfolios.header" | translate }}</p>
|
||||
<h1>{{ 'portfolios.new.title' | translate }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
|
||||
<base-form inline-template>
|
||||
<div class="row">
|
||||
@ -38,13 +39,18 @@
|
||||
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<div class='action-group-footer'>
|
||||
{% block next_button %}
|
||||
{{ SaveButton(text=('portfolios.new.save' | translate), form="portfolio-create", element="input") }}
|
||||
{% endblock %}
|
||||
<a class="usa-button usa-button-secondary" href="{{ url_for('atst.home') }}">
|
||||
Cancel
|
||||
</a>
|
||||
<div
|
||||
class='action-group-footer'
|
||||
v-bind:class="{'action-group-footer--expand-offset': this.$root.sidenavExpanded, 'action-group-footer--collapse-offset': !this.$root.sidenavExpanded}">
|
||||
<div class="action-group-footer--container">
|
||||
{% block next_button %}
|
||||
{{ SaveButton(text=('portfolios.new.save' | translate), form="portfolio-create", element="input") }}
|
||||
{% endblock %}
|
||||
<a class="usa-button usa-button-secondary" href="{{ url_for('atst.home') }}">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</base-form>
|
||||
|
@ -31,7 +31,10 @@
|
||||
<div class="task-order">
|
||||
{% block to_builder_form_field %}{% endblock %}
|
||||
</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 %}
|
||||
<input
|
||||
type="submit"
|
||||
@ -58,14 +61,13 @@
|
||||
</a>
|
||||
{%- endif %}
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
v-on:click="openModal('cancel')"
|
||||
class="action-group__action icon-link">
|
||||
{{ "common.cancel" | translate }}
|
||||
</a>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</to-form>
|
||||
{% endblock %}
|
||||
|
@ -2,6 +2,8 @@ import json
|
||||
from unittest.mock import Mock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pendulum
|
||||
import pydantic
|
||||
import pytest
|
||||
from tests.factories import ApplicationFactory, EnvironmentFactory
|
||||
from tests.mock_azure import AUTH_CREDENTIALS, mock_azure
|
||||
@ -20,10 +22,16 @@ from atst.domain.csp.cloud.models import (
|
||||
BillingProfileTenantAccessCSPResult,
|
||||
BillingProfileVerificationCSPPayload,
|
||||
BillingProfileVerificationCSPResult,
|
||||
CostManagementQueryCSPResult,
|
||||
EnvironmentCSPPayload,
|
||||
EnvironmentCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
ProductPurchaseCSPPayload,
|
||||
ProductPurchaseCSPResult,
|
||||
ProductPurchaseVerificationCSPPayload,
|
||||
ProductPurchaseVerificationCSPResult,
|
||||
ReportingCSPPayload,
|
||||
SubscriptionCreationCSPPayload,
|
||||
SubscriptionCreationCSPResult,
|
||||
SubscriptionVerificationCSPPayload,
|
||||
@ -44,7 +52,10 @@ from atst.domain.csp.cloud.models import (
|
||||
TenantPrincipalCSPResult,
|
||||
TenantPrincipalOwnershipCSPPayload,
|
||||
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"
|
||||
|
||||
@ -57,12 +68,14 @@ def mock_management_group_create(mock_azure, spec_dict):
|
||||
|
||||
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
|
||||
environment = EnvironmentFactory.create()
|
||||
|
||||
mock_management_group_create(mock_azure, {"id": "Test Id"})
|
||||
|
||||
result = mock_azure.create_environment(
|
||||
AUTH_CREDENTIALS, environment.creator, environment
|
||||
mock_azure = mock_get_secret(mock_azure)
|
||||
|
||||
payload = EnvironmentCSPPayload(
|
||||
tenant_id="1234", display_name=environment.name, parent_id=str(uuid4())
|
||||
)
|
||||
result = mock_azure.create_environment(payload)
|
||||
|
||||
assert result.id == "Test Id"
|
||||
|
||||
@ -97,20 +110,6 @@ def test_create_application_succeeds(mock_azure: AzureCloudProvider):
|
||||
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):
|
||||
subscription_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"
|
||||
|
||||
|
||||
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):
|
||||
mock_azure.sdk.adal.AuthenticationContext.return_value.context.acquire_token_with_client_credentials.return_value = {
|
||||
"accessToken": "TOKEN"
|
||||
@ -573,10 +593,10 @@ def test_create_tenant_principal_credential(mock_azure: AzureCloudProvider):
|
||||
def test_create_admin_role_definition(mock_azure: AzureCloudProvider):
|
||||
with patch.object(
|
||||
AzureCloudProvider,
|
||||
"_get_elevated_management_token",
|
||||
wraps=mock_azure._get_elevated_management_token,
|
||||
) as get_elevated_management_token:
|
||||
get_elevated_management_token.return_value = "my fake token"
|
||||
"_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
|
||||
@ -657,6 +677,35 @@ def test_create_tenant_principal_ownership(mock_azure: AzureCloudProvider):
|
||||
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):
|
||||
with patch.object(
|
||||
AzureCloudProvider,
|
||||
@ -718,3 +767,224 @@ def test_create_subscription_verification(mock_azure: AzureCloudProvider):
|
||||
payload
|
||||
)
|
||||
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
|
||||
|
||||
from atst.domain.csp import MockCloudProvider
|
||||
from atst.domain.csp.cloud.models import EnvironmentCSPPayload, EnvironmentCSPResult
|
||||
|
||||
from tests.factories import EnvironmentFactory, EnvironmentRoleFactory, UserFactory
|
||||
|
||||
@ -14,20 +15,17 @@ def mock_csp():
|
||||
|
||||
def test_create_environment(mock_csp: MockCloudProvider):
|
||||
environment = EnvironmentFactory.create()
|
||||
user = UserFactory.create()
|
||||
environment_id = mock_csp.create_environment(CREDENTIALS, user, environment)
|
||||
assert isinstance(environment_id, str)
|
||||
|
||||
|
||||
def test_create_admin_user(mock_csp: MockCloudProvider):
|
||||
admin_user = mock_csp.create_atat_admin_user(CREDENTIALS, "env_id")
|
||||
assert isinstance(admin_user["id"], str)
|
||||
assert isinstance(admin_user["credentials"], dict)
|
||||
|
||||
|
||||
def test_create_environment_baseline(mock_csp: MockCloudProvider):
|
||||
baseline = mock_csp.create_atat_admin_user(CREDENTIALS, "env_id")
|
||||
assert isinstance(baseline, dict)
|
||||
environment.application.cloud_id = "parent_id"
|
||||
environment.application.portfolio.csp_data = {"tenant_id": "fake"}
|
||||
payload = EnvironmentCSPPayload(
|
||||
**dict(
|
||||
tenant_id=environment.application.portfolio.csp_data.get("tenant_id"),
|
||||
display_name=environment.name,
|
||||
parent_id=environment.application.cloud_id,
|
||||
)
|
||||
)
|
||||
result = mock_csp.create_environment(payload)
|
||||
assert isinstance(result, EnvironmentCSPResult)
|
||||
|
||||
|
||||
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):
|
||||
environment = EnvironmentFactory.create()
|
||||
assert environment.is_pending
|
||||
assert not environment.cloud_id
|
||||
env_role1 = EnvironmentRoleFactory.create(environment=environment)
|
||||
env_role1 = EnvironmentRoles.disable(env_role1.id)
|
||||
assert env_role1.disabled
|
||||
|
||||
environment.cloud_id = "cloud-id"
|
||||
environment.root_user_info = {"credentials": "credentials"}
|
||||
session.add(environment)
|
||||
session.commit()
|
||||
session.refresh(environment)
|
||||
|
||||
assert not environment.is_pending
|
||||
assert environment.cloud_id
|
||||
env_role2 = EnvironmentRoleFactory.create(environment=environment)
|
||||
env_role2 = EnvironmentRoles.disable(env_role2.id)
|
||||
assert env_role2.disabled
|
||||
|
||||
|
||||
def test_disable_checks_env_role_provisioning_status():
|
||||
environment = EnvironmentFactory.create(
|
||||
cloud_id="cloud-id", root_user_info={"credentials": "credentials"}
|
||||
)
|
||||
environment = EnvironmentFactory.create(cloud_id="cloud-id")
|
||||
environment.application.portfolio.csp_data = {"tenant_id": uuid4().hex}
|
||||
env_role1 = EnvironmentRoleFactory.create(environment=environment)
|
||||
assert not env_role1.cloud_id
|
||||
env_role1 = EnvironmentRoles.disable(env_role1.id)
|
||||
|
@ -1,5 +1,4 @@
|
||||
import pytest
|
||||
import pendulum
|
||||
from uuid import uuid4
|
||||
|
||||
from atst.domain.environments import Environments
|
||||
@ -14,6 +13,7 @@ from tests.factories import (
|
||||
EnvironmentRoleFactory,
|
||||
ApplicationRoleFactory,
|
||||
)
|
||||
from tests.utils import EnvQueryTest
|
||||
|
||||
|
||||
def test_create_environments():
|
||||
@ -119,40 +119,6 @@ def test_update_does_not_duplicate_names_within_application():
|
||||
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):
|
||||
def test_with_expired_clins(self, session):
|
||||
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)])
|
||||
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):
|
||||
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
|
||||
|
||||
|
||||
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,
|
||||
get_all_portfolio_permission_sets,
|
||||
)
|
||||
from tests.utils import EnvQueryTest
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@ -263,10 +264,44 @@ def test_create_state_machine(portfolio):
|
||||
assert fsm
|
||||
|
||||
|
||||
def test_get_portfolios_pending_provisioning(session):
|
||||
for x in range(5):
|
||||
portfolio = PortfolioFactory.create()
|
||||
sm = PortfolioStateMachineFactory.create(portfolio=portfolio)
|
||||
if x == 2:
|
||||
sm.state = FSMStates.COMPLETED
|
||||
assert len(Portfolios.get_portfolios_pending_provisioning()) == 4
|
||||
class TestGetPortfoliosPendingCreate(EnvQueryTest):
|
||||
def test_finds_unstarted(self):
|
||||
for x in range(5):
|
||||
if x == 2:
|
||||
state = "COMPLETED"
|
||||
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
|
||||
|
||||
|
||||
@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():
|
||||
member_list = [
|
||||
{"role_name": CSPRole.ADMIN},
|
||||
|
@ -14,7 +14,6 @@ from atst.jobs import (
|
||||
dispatch_create_application,
|
||||
dispatch_create_user,
|
||||
dispatch_create_environment_role,
|
||||
dispatch_create_atat_admin_user,
|
||||
dispatch_provision_portfolio,
|
||||
create_environment,
|
||||
do_create_user,
|
||||
@ -22,7 +21,6 @@ from atst.jobs import (
|
||||
do_create_environment,
|
||||
do_create_environment_role,
|
||||
do_create_application,
|
||||
do_create_atat_admin_user,
|
||||
)
|
||||
from tests.factories import (
|
||||
EnvironmentFactory,
|
||||
@ -97,6 +95,10 @@ tomorrow = now.add(days=1)
|
||||
|
||||
def test_create_environment_job(session, csp):
|
||||
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)
|
||||
session.refresh(environment)
|
||||
|
||||
@ -152,19 +154,11 @@ def test_create_user_job(session, csp):
|
||||
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):
|
||||
# Given that I have a portfolio with an active CLIN and two environments,
|
||||
# one of which is deleted
|
||||
portfolio = PortfolioFactory.create(
|
||||
applications=[{"environments": [{}, {}]}],
|
||||
applications=[{"environments": [{}, {}], "cloud_id": uuid4().hex}],
|
||||
task_orders=[
|
||||
{
|
||||
"create_clins": [
|
||||
@ -230,36 +224,9 @@ def test_dispatch_create_user(monkeypatch):
|
||||
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):
|
||||
portfolio = PortfolioFactory.create(
|
||||
applications=[
|
||||
{"environments": [{"cloud_id": uuid4().hex, "root_user_info": {}}]}
|
||||
],
|
||||
applications=[{"environments": [{"cloud_id": uuid4().hex}]}],
|
||||
task_orders=[
|
||||
{
|
||||
"create_clins": [
|
||||
@ -289,9 +256,19 @@ def test_create_environment_no_dupes(session, celery_app, celery_worker):
|
||||
assert environment.claimed_until == None
|
||||
|
||||
|
||||
def test_dispatch_provision_portfolio(
|
||||
csp, session, portfolio, celery_app, celery_worker, monkeypatch
|
||||
):
|
||||
def test_dispatch_provision_portfolio(csp, 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)
|
||||
mock = Mock()
|
||||
monkeypatch.setattr("atst.jobs.provision_portfolio", mock)
|
||||
|
@ -5,9 +5,12 @@ from unittest.mock import Mock
|
||||
from OpenSSL import crypto
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from flask import template_rendered
|
||||
import pendulum
|
||||
|
||||
from atst.utils.notification_sender import NotificationSender
|
||||
|
||||
import tests.factories as factories
|
||||
|
||||
|
||||
@contextmanager
|
||||
def captured_templates(app):
|
||||
@ -62,3 +65,46 @@ def make_crl_list(x509_obj, x509_path):
|
||||
issuer = x509_obj.issuer.public_bytes(default_backend())
|
||||
filename = os.path.basename(x509_path)
|
||||
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}
|
||||
resent:
|
||||
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:
|
||||
removed:
|
||||
title: Team member removed from application
|
||||
@ -166,6 +163,9 @@ flash:
|
||||
errors:
|
||||
title: There were some errors
|
||||
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_title: Log in required
|
||||
logged_out:
|
||||
|
Loading…
x
Reference in New Issue
Block a user