Merge pull request #1423 from dod-ccpo/notify-ppoc

Notify PPOC when portfolio is provisioned
This commit is contained in:
graham-dds 2020-02-13 11:20:26 -05:00 committed by GitHub
commit 6af28ed655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 94 additions and 19 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

@ -327,9 +327,7 @@ class AzureCloudProvider(CloudProviderInterface):
if result.status_code == 200: if result.status_code == 200:
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(

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

@ -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

@ -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

@ -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

@ -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."