merge staging

This commit is contained in:
Philip Kalinsky 2020-02-13 12:49:29 -05:00
commit 89d56d55a1
17 changed files with 132 additions and 27 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$", "files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null "lines": null
}, },
"generated_at": "2020-02-10T21:40:38Z", "generated_at": "2020-02-12T18:51:01Z",
"plugins_used": [ "plugins_used": [
{ {
"base64_limit": 4.5, "base64_limit": 4.5,
@ -82,7 +82,7 @@
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_secret": false, "is_secret": false,
"is_verified": false, "is_verified": false,
"line_number": 43, "line_number": 44,
"type": "Secret Keyword" "type": "Secret Keyword"
} }
], ],

View File

@ -219,6 +219,7 @@ To generate coverage reports for the Javascript tests:
- `ASSETS_URL`: URL to host which serves static assets (such as a CDN). - `ASSETS_URL`: URL to host which serves static assets (such as a CDN).
- `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account - `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account
- `AZURE_LOGIN_URL`: The URL used to login for an Azure instance.
- `AZURE_STORAGE_KEY`: A valid secret key for the Azure blob storage account - `AZURE_STORAGE_KEY`: A valid secret key for the Azure blob storage account
- `AZURE_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads - `AZURE_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads
- `BLOB_STORAGE_URL`: URL to Azure blob storage container. - `BLOB_STORAGE_URL`: URL to Azure blob storage container.

View File

@ -186,6 +186,7 @@ def map_config(config):
# with a Beat job once a day) # with a Beat job once a day)
"CELERY_RESULT_EXPIRES": 0, "CELERY_RESULT_EXPIRES": 0,
"CELERY_RESULT_EXTENDED": True, "CELERY_RESULT_EXTENDED": True,
"OFFICE_365_DOMAIN": "onmicrosoft.com",
"CONTRACT_START_DATE": datetime.strptime( "CONTRACT_START_DATE": datetime.strptime(
config.get("default", "CONTRACT_START_DATE"), "%Y-%m-%d" config.get("default", "CONTRACT_START_DATE"), "%Y-%m-%d"
).date(), ).date(),

View File

@ -331,6 +331,7 @@ class AzureCloudProvider(CloudProviderInterface):
timeout=30, timeout=30,
) )
result.raise_for_status() result.raise_for_status()
except self.sdk.requests.exceptions.ConnectionError: except self.sdk.requests.exceptions.ConnectionError:
app.logger.error( app.logger.error(
f"Could not create tenant. Connection Error", exc_info=1, f"Could not create tenant. Connection Error", exc_info=1,
@ -354,10 +355,7 @@ class AzureCloudProvider(CloudProviderInterface):
result_dict = result.json() result_dict = result.json()
tenant_id = result_dict.get("tenantId") tenant_id = result_dict.get("tenantId")
tenant_admin_username = ( tenant_admin_username = f"{payload.user_id}@{payload.domain_name}.{self.config.get('OFFICE_365_DOMAIN')}"
f"{payload.user_id}@{payload.domain_name}.onmicrosoft.com"
)
self.update_tenant_creds( self.update_tenant_creds(
tenant_id, tenant_id,
KeyVaultCredentials( KeyVaultCredentials(
@ -366,6 +364,7 @@ class AzureCloudProvider(CloudProviderInterface):
tenant_admin_password=payload.password, tenant_admin_password=payload.password,
), ),
) )
return TenantCSPResult(domain_name=payload.domain_name, **result_dict) return TenantCSPResult(domain_name=payload.domain_name, **result_dict)
def create_billing_profile_creation( def create_billing_profile_creation(

View File

@ -4,6 +4,7 @@ from typing import Dict, List, Optional
from uuid import uuid4 from uuid import uuid4
import re import re
from flask import current_app as app
from pydantic import BaseModel, validator, root_validator from pydantic import BaseModel, validator, root_validator
from atst.utils import snake_to_camel from atst.utils import snake_to_camel
@ -526,7 +527,7 @@ class UserMixin(BaseModel):
@property @property
def user_principal_name(self): def user_principal_name(self):
return f"{self.mail_nickname}@{self.tenant_host_name}.onmicrosoft.com" return f"{self.mail_nickname}@{self.tenant_host_name}.{app.config.get('OFFICE_365_DOMAIN')}"
@property @property
def mail_nickname(self): def mail_nickname(self):

View File

@ -10,6 +10,10 @@ SERVICE_BRANCHES = [
translate("forms.portfolio.defense_component.choices.marine_corps"), translate("forms.portfolio.defense_component.choices.marine_corps"),
), ),
("navy", translate("forms.portfolio.defense_component.choices.navy")), ("navy", translate("forms.portfolio.defense_component.choices.navy")),
("space_force", translate("forms.portfolio.defense_component.choices.space_force")),
("ccmd_js", translate("forms.portfolio.defense_component.choices.ccmd_js")),
("dafa", translate("forms.portfolio.defense_component.choices.dafa")),
("osd_psas", translate("forms.portfolio.defense_component.choices.osd_psas")),
("other", translate("forms.portfolio.defense_component.choices.other")), ("other", translate("forms.portfolio.defense_component.choices.other")),
] ]

View File

@ -18,6 +18,7 @@ from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.models import CSPRole, JobFailure from atst.models import CSPRole, JobFailure
from atst.models.mixins.state_machines import FSMStates
from atst.domain.task_orders import TaskOrders from atst.domain.task_orders import TaskOrders
from atst.models.utils import claim_for_update, claim_many_for_update from atst.models.utils import claim_for_update, claim_many_for_update
from atst.queue import celery from atst.queue import celery
@ -177,10 +178,30 @@ def do_work(fn, task, csp, **kwargs):
raise task.retry(exc=e) raise task.retry(exc=e)
def send_PPOC_email(portfolio_dict):
ppoc_email = portfolio_dict.get("password_recovery_email_address")
user_id = portfolio_dict.get("user_id")
domain_name = portfolio_dict.get("domain_name")
send_mail(
recipients=[ppoc_email],
subject=translate("email.portfolio_ready.subject"),
body=translate(
"email.portfolio_ready.body",
{
"password_reset_address": app.config.get("AZURE_LOGIN_URL"),
"username": f"{user_id}@{domain_name}.{app.config.get('OFFICE_365_DOMAIN')}",
},
),
)
def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None): def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None):
portfolio = Portfolios.get_for_update(portfolio_id) portfolio = Portfolios.get_for_update(portfolio_id)
fsm = Portfolios.get_or_create_state_machine(portfolio) fsm = Portfolios.get_or_create_state_machine(portfolio)
fsm.trigger_next_transition(csp_data=portfolio.to_dictionary()) fsm.trigger_next_transition(csp_data=portfolio.to_dictionary())
if fsm.current_state == FSMStates.COMPLETED:
send_PPOC_email(portfolio.to_dictionary())
@celery.task(bind=True, base=RecordFailure) @celery.task(bind=True, base=RecordFailure)

View File

@ -58,6 +58,18 @@ def _build_transitions(csp_stages):
transitions = [] transitions = []
states = [] states = []
for stage_i, csp_stage in enumerate(csp_stages): for stage_i, csp_stage in enumerate(csp_stages):
# the last CREATED stage has a transition to COMPLETED
if stage_i == len(csp_stages) - 1:
transitions.append(
dict(
trigger="complete",
source=compose_state(
list(csp_stages)[stage_i], StageStates.CREATED
),
dest=FSMStates.COMPLETED,
)
)
for state in StageStates: for state in StageStates:
states.append( states.append(
dict( dict(

View File

@ -161,6 +161,15 @@ class PortfolioStateMachine(
) )
elif state_obj.is_CREATED: elif state_obj.is_CREATED:
# if last CREATED state then transition to COMPLETED
if list(AzureStages)[-1].name == state_obj.name.split("_CREATED")[
0
] and "complete" in self.machine.get_triggers(state_obj.name):
app.logger.info(
"last stage completed. transitioning to COMPLETED state"
)
self.trigger("complete", **kwargs)
# the create trigger for the next stage should be in the available # the create trigger for the next stage should be in the available
# triggers for the current state # triggers for the current state
create_trigger = next( create_trigger = next(

View File

@ -4,6 +4,7 @@ AZURE_AADP_QTY=5
AZURE_ACCOUNT_NAME AZURE_ACCOUNT_NAME
AZURE_CLIENT_ID AZURE_CLIENT_ID
AZURE_GRAPH_RESOURCE="https://graph.microsoft.com/" AZURE_GRAPH_RESOURCE="https://graph.microsoft.com/"
AZURE_LOGIN_URL="https://portal.azure.com/"
AZURE_POLICY_LOCATION=policies AZURE_POLICY_LOCATION=policies
AZURE_POWERSHELL_CLIENT_ID AZURE_POWERSHELL_CLIENT_ID
AZURE_ROLE_DEF_ID_BILLING_READER="fa23ad8b-c56e-40d8-ac0c-ce449e1d2c64" AZURE_ROLE_DEF_ID_BILLING_READER="fa23ad8b-c56e-40d8-ac0c-ce449e1d2c64"

View File

@ -72,7 +72,7 @@ export default {
const uploader = await this.getUploader() const uploader = await this.getUploader()
const response = await uploader.upload(file) const response = await uploader.upload(file)
if (uploadResponseOkay(response)) { if (uploadResponseOkay(response)) {
this.attachment = e.target.value this.attachment = file.name
this.objectName = uploader.objectName this.objectName = uploader.objectName
this.$refs.attachmentFilename.value = file.name this.$refs.attachmentFilename.value = file.name
this.$refs.attachmentObjectName.value = response.objectName this.$refs.attachmentObjectName.value = response.objectName

View File

@ -43,7 +43,6 @@
:id="name" :id="name"
:name="name" :name="name"
aria-label="Task Order Upload" aria-label="Task Order Upload"
v-bind:value="attachment"
type="file"> type="file">
<input type="hidden" name="{{ field.filename.name }}" id="{{ field.filename.name }}" ref="attachmentFilename"> <input type="hidden" name="{{ field.filename.name }}" id="{{ field.filename.name }}" ref="attachmentFilename">
<input type="hidden" name="{{ field.object_name.name }}" id="{{ field.object_name.name }}" ref="attachmentObjectName" v-bind:value='objectName'> <input type="hidden" name="{{ field.object_name.name }}" id="{{ field.object_name.name }}" ref="attachmentObjectName" v-bind:value='objectName'>

View File

@ -21,10 +21,10 @@
</template> </template>
</div> </div>
<ul class="sidenav__list" v-if="isVisible"> <ul class="sidenav__list" v-if="isVisible">
{% for other_portfolio in portfolios|sort(attribute='name') %} {% for portfolio in portfolios|sort(attribute='name') %}
{{ SidenavItem(other_portfolio.name, {{ SidenavItem(portfolio.name,
href=url_for("applications.portfolio_applications", portfolio_id=other_portfolio.id), href=url_for("applications.portfolio_applications", portfolio_id=portfolio.id),
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id') active=portfolio == g.portfolio
) }} ) }}
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -134,9 +134,12 @@ def test_UserCSPPayload_mail_nickname():
assert payload.mail_nickname == f"han.solo" assert payload.mail_nickname == f"han.solo"
def test_UserCSPPayload_user_principal_name(): def test_UserCSPPayload_user_principal_name(app):
payload = UserCSPPayload(**user_payload) payload = UserCSPPayload(**user_payload)
assert payload.user_principal_name == f"han.solo@rebelalliance.onmicrosoft.com" assert (
payload.user_principal_name
== f"han.solo@rebelalliance.{app.config.get('OFFICE_365_DOMAIN')}"
)
def test_UserCSPPayload_password(): def test_UserCSPPayload_password():
@ -167,11 +170,11 @@ class TestBillingOwnerCSPPayload:
payload = BillingOwnerCSPPayload(**self.user_payload) payload = BillingOwnerCSPPayload(**self.user_payload)
assert payload.password assert payload.password
def test_user_principal_name(self): def test_user_principal_name(self, app):
payload = BillingOwnerCSPPayload(**self.user_payload) payload = BillingOwnerCSPPayload(**self.user_payload)
assert ( assert (
payload.user_principal_name payload.user_principal_name
== f"billing_admin@rebelalliance.onmicrosoft.com" == f"billing_admin@rebelalliance.{app.config.get('OFFICE_365_DOMAIN')}"
) )
def test_email(self): def test_email(self):

View File

@ -216,6 +216,7 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
FSMStates.TENANT_ADMIN_OWNERSHIP_CREATED, FSMStates.TENANT_ADMIN_OWNERSHIP_CREATED,
FSMStates.TENANT_PRINCIPAL_OWNERSHIP_CREATED, FSMStates.TENANT_PRINCIPAL_OWNERSHIP_CREATED,
FSMStates.BILLING_OWNER_CREATED, FSMStates.BILLING_OWNER_CREATED,
FSMStates.COMPLETED,
] ]
if portfolio.csp_data is not None: if portfolio.csp_data is not None:
@ -239,7 +240,7 @@ def test_fsm_transition_start(mock_cloud_provider, portfolio: Portfolio):
"first_name": ppoc.first_name, "first_name": ppoc.first_name,
"last_name": ppoc.last_name, "last_name": ppoc.last_name,
"country_code": "US", "country_code": "US",
"password_recovery_email_address": "email@example.com", # ppoc.email, "password_recovery_email_address": ppoc.email,
"address": { # TODO: TBD if we're sourcing this from data or config "address": { # TODO: TBD if we're sourcing this from data or config
"company_name": "", "company_name": "",
"address_line_1": "", "address_line_1": "",

View File

@ -7,8 +7,7 @@ from azure.core.exceptions import AzureError
from atst.domain.csp.cloud import MockCloudProvider from atst.domain.csp.cloud import MockCloudProvider
from atst.domain.csp.cloud.models import UserRoleCSPResult from atst.domain.csp.cloud.models import UserRoleCSPResult
from atst.domain.portfolios import Portfolios from atst.models import ApplicationRoleStatus, Portfolio, FSMStates
from atst.models import ApplicationRoleStatus
from atst.jobs import ( from atst.jobs import (
RecordFailure, RecordFailure,
@ -24,6 +23,7 @@ from atst.jobs import (
do_create_environment, do_create_environment,
do_create_environment_role, do_create_environment_role,
do_create_application, do_create_application,
send_PPOC_email,
) )
from tests.factories import ( from tests.factories import (
ApplicationFactory, ApplicationFactory,
@ -135,11 +135,11 @@ def test_create_application_job_is_idempotent(csp):
csp.create_application.assert_not_called() csp.create_application.assert_not_called()
def test_create_user_job(session, csp): def test_create_user_job(session, csp, app):
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(
csp_data={ csp_data={
"tenant_id": str(uuid4()), "tenant_id": str(uuid4()),
"domain_name": "rebelalliance.onmicrosoft.com", "domain_name": f"rebelalliance.{app.config.get('OFFICE_365_DOMAIN')}",
} }
) )
application = ApplicationFactory.create(portfolio=portfolio, cloud_id="321") application = ApplicationFactory.create(portfolio=portfolio, cloud_id="321")
@ -281,11 +281,58 @@ def test_dispatch_provision_portfolio(csp, monkeypatch):
mock.delay.assert_called_once_with(portfolio_id=portfolio.id) mock.delay.assert_called_once_with(portfolio_id=portfolio.id)
def test_do_provision_portfolio(csp, session, portfolio): class TestDoProvisionPortfolio:
def test_portfolio_has_state_machine(self, csp, session, portfolio):
do_provision_portfolio(csp=csp, portfolio_id=portfolio.id) do_provision_portfolio(csp=csp, portfolio_id=portfolio.id)
session.refresh(portfolio) session.refresh(portfolio)
assert portfolio.state_machine assert portfolio.state_machine
def test_sends_email_to_PPOC_on_completion(
self, monkeypatch, csp, portfolio: Portfolio
):
mock = Mock()
monkeypatch.setattr("atst.jobs.send_PPOC_email", mock)
csp._authorize.return_value = None
csp._maybe_raise.return_value = None
sm: PortfolioStateMachine = PortfolioStateMachineFactory.create(
portfolio=portfolio
)
# The stage before "COMPLETED"
sm.state = FSMStates.BILLING_OWNER_CREATED
do_provision_portfolio(csp=csp, portfolio_id=portfolio.id)
# send_PPOC_email was called
assert mock.assert_called_once
def test_send_ppoc_email(monkeypatch, app):
mock = Mock()
monkeypatch.setattr("atst.jobs.send_mail", mock)
ppoc_email = "example@example.com"
user_id = "user_id"
domain_name = "domain"
send_PPOC_email(
{
"password_recovery_email_address": ppoc_email,
"user_id": user_id,
"domain_name": domain_name,
}
)
mock.assert_called_once_with(
recipients=[ppoc_email],
subject=translate("email.portfolio_ready.subject"),
body=translate(
"email.portfolio_ready.body",
{
"password_reset_address": app.config.get("AZURE_LOGIN_URL"),
"username": f"{user_id}@{domain_name}.{app.config.get('OFFICE_365_DOMAIN')}",
},
),
)
def test_provision_portfolio_create_tenant( def test_provision_portfolio_create_tenant(
csp, session, portfolio, celery_app, celery_worker, monkeypatch csp, session, portfolio, celery_app, celery_worker, monkeypatch

View File

@ -83,7 +83,9 @@ errors:
email: email:
application_invite: "{inviter_name} has invited you to a JEDI cloud application" application_invite: "{inviter_name} has invited you to a JEDI cloud application"
portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio" portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio"
environment_ready: JEDI cloud environment ready portfolio_ready:
subject: Portfolio Provisioned
body: "Your portfolio has been provisioned.\nVisit {password_reset_address}, and use your username, {username}, to create a password."
task_order_sent: task_order_sent:
subject: "Task Order {to_number}" subject: "Task Order {to_number}"
body: "Task Order number {to_number} updated." body: "Task Order number {to_number} updated."
@ -296,6 +298,10 @@ forms:
army: Army army: Army
marine_corps: Marine Corps marine_corps: Marine Corps
navy: Navy navy: Navy
space_force: Space Force
ccmd_js: Combatant Command / Joint Staff (CCMD/JS)
dafa: Defense Agency and Field Activity (DAFA)
osd_psas: Office of the Secretary of Defense (OSD) / Principal Staff Assistants (PSAs)
other: Other other: Other
title: Select DoD component(s) funding your Portfolio title: Select DoD component(s) funding your Portfolio
validation_message: You must select at least one defense component. validation_message: You must select at least one defense component.