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$",
"lines": null
},
"generated_at": "2020-02-10T21:40:38Z",
"generated_at": "2020-02-12T18:51:01Z",
"plugins_used": [
{
"base64_limit": 4.5,
@ -82,7 +82,7 @@
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_secret": false,
"is_verified": false,
"line_number": 43,
"line_number": 44,
"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).
- `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_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads
- `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)
"CELERY_RESULT_EXPIRES": 0,
"CELERY_RESULT_EXTENDED": True,
"OFFICE_365_DOMAIN": "onmicrosoft.com",
"CONTRACT_START_DATE": datetime.strptime(
config.get("default", "CONTRACT_START_DATE"), "%Y-%m-%d"
).date(),

View File

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

View File

@ -4,6 +4,7 @@ from typing import Dict, List, Optional
from uuid import uuid4
import re
from flask import current_app as app
from pydantic import BaseModel, validator, root_validator
from atst.utils import snake_to_camel
@ -526,7 +527,7 @@ class UserMixin(BaseModel):
@property
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
def mail_nickname(self):

View File

@ -10,6 +10,10 @@ SERVICE_BRANCHES = [
translate("forms.portfolio.defense_component.choices.marine_corps"),
),
("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")),
]

View File

@ -18,6 +18,7 @@ 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.mixins.state_machines import FSMStates
from atst.domain.task_orders import TaskOrders
from atst.models.utils import claim_for_update, claim_many_for_update
from atst.queue import celery
@ -177,10 +178,30 @@ def do_work(fn, task, csp, **kwargs):
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):
portfolio = Portfolios.get_for_update(portfolio_id)
fsm = Portfolios.get_or_create_state_machine(portfolio)
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)

View File

@ -58,6 +58,18 @@ def _build_transitions(csp_stages):
transitions = []
states = []
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:
states.append(
dict(

View File

@ -161,6 +161,15 @@ class PortfolioStateMachine(
)
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
# triggers for the current state
create_trigger = next(

View File

@ -4,6 +4,7 @@ AZURE_AADP_QTY=5
AZURE_ACCOUNT_NAME
AZURE_CLIENT_ID
AZURE_GRAPH_RESOURCE="https://graph.microsoft.com/"
AZURE_LOGIN_URL="https://portal.azure.com/"
AZURE_POLICY_LOCATION=policies
AZURE_POWERSHELL_CLIENT_ID
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 response = await uploader.upload(file)
if (uploadResponseOkay(response)) {
this.attachment = e.target.value
this.attachment = file.name
this.objectName = uploader.objectName
this.$refs.attachmentFilename.value = file.name
this.$refs.attachmentObjectName.value = response.objectName

View File

@ -43,7 +43,6 @@
:id="name"
:name="name"
aria-label="Task Order Upload"
v-bind:value="attachment"
type="file">
<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'>

View File

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

View File

@ -134,9 +134,12 @@ def test_UserCSPPayload_mail_nickname():
assert payload.mail_nickname == f"han.solo"
def test_UserCSPPayload_user_principal_name():
def test_UserCSPPayload_user_principal_name(app):
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():
@ -167,11 +170,11 @@ class TestBillingOwnerCSPPayload:
payload = BillingOwnerCSPPayload(**self.user_payload)
assert payload.password
def test_user_principal_name(self):
def test_user_principal_name(self, app):
payload = BillingOwnerCSPPayload(**self.user_payload)
assert (
payload.user_principal_name
== f"billing_admin@rebelalliance.onmicrosoft.com"
== f"billing_admin@rebelalliance.{app.config.get('OFFICE_365_DOMAIN')}"
)
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_PRINCIPAL_OWNERSHIP_CREATED,
FSMStates.BILLING_OWNER_CREATED,
FSMStates.COMPLETED,
]
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,
"last_name": ppoc.last_name,
"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
"company_name": "",
"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.models import UserRoleCSPResult
from atst.domain.portfolios import Portfolios
from atst.models import ApplicationRoleStatus
from atst.models import ApplicationRoleStatus, Portfolio, FSMStates
from atst.jobs import (
RecordFailure,
@ -24,6 +23,7 @@ from atst.jobs import (
do_create_environment,
do_create_environment_role,
do_create_application,
send_PPOC_email,
)
from tests.factories import (
ApplicationFactory,
@ -135,11 +135,11 @@ def test_create_application_job_is_idempotent(csp):
csp.create_application.assert_not_called()
def test_create_user_job(session, csp):
def test_create_user_job(session, csp, app):
portfolio = PortfolioFactory.create(
csp_data={
"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")
@ -281,10 +281,57 @@ def test_dispatch_provision_portfolio(csp, monkeypatch):
mock.delay.assert_called_once_with(portfolio_id=portfolio.id)
def test_do_provision_portfolio(csp, session, portfolio):
do_provision_portfolio(csp=csp, portfolio_id=portfolio.id)
session.refresh(portfolio)
assert portfolio.state_machine
class TestDoProvisionPortfolio:
def test_portfolio_has_state_machine(self, csp, session, portfolio):
do_provision_portfolio(csp=csp, portfolio_id=portfolio.id)
session.refresh(portfolio)
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(

View File

@ -83,7 +83,9 @@ errors:
email:
application_invite: "{inviter_name} has invited you to a JEDI cloud application"
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:
subject: "Task Order {to_number}"
body: "Task Order number {to_number} updated."
@ -296,6 +298,10 @@ forms:
army: Army
marine_corps: Marine Corps
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
title: Select DoD component(s) funding your Portfolio
validation_message: You must select at least one defense component.