From 2c37b169631f2f503e888212a592f917f741c075 Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 18 Feb 2020 13:26:41 -0500 Subject: [PATCH 1/4] Send email to user when env role is created --- atst/jobs.py | 15 +++++++++++++- tests/test_jobs.py | 51 ++++++++++++++++++++++++++++++++-------------- translations.yaml | 5 ++++- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/atst/jobs.py b/atst/jobs.py index 80585d3a..1dbff894 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -177,7 +177,20 @@ def do_create_environment_role(csp: CloudProviderInterface, environment_role_id= env_role.cloud_id = result.id db.session.add(env_role) db.session.commit() - # TODO: should send notification email to the user, maybe with their portal login name + user = env_role.application_role.user + mail_name = user.full_name.replace(" ", ".").lower() + username = f"{mail_name}@{csp_details.get('tennant_id')}.{app.config.get('OFFICE_365_DOMAIN')}" + send_mail( + recipients=[user.email], + subject=translate("email.azure_account_update.subject"), + body=translate( + "email.azure_account_update.body", + {"url": app.config.get("AZURE_LOGIN_URL"), "username": username}, + ), + ) + app.logger.info( + f"Notification email sent for enivornment role creation. User id: {user.id}" + ) def render_email(template_path, context): diff --git a/tests/test_jobs.py b/tests/test_jobs.py index eba01f9b..8fc7df12 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -399,23 +399,44 @@ def test_dispatch_create_environment_role(monkeypatch): mock.delay.assert_called_once_with(environment_role_id=env_role.id) -def test_create_environment_role(): - portfolio = PortfolioFactory.create(csp_data={"tenant_id": "123"}) - app = ApplicationFactory.create(portfolio=portfolio) - app_role = ApplicationRoleFactory.create( - application=app, status=ApplicationRoleStatus.ACTIVE, cloud_id="123", - ) - env = EnvironmentFactory.create(application=app, cloud_id="123") - env_role = EnvironmentRoleFactory.create( - environment=env, application_role=app_role, cloud_id=None - ) +class TestCreateEnvironmentRole: + def test_success(self): + portfolio = PortfolioFactory.create(csp_data={"tenant_id": "123"}) + app = ApplicationFactory.create(portfolio=portfolio) + app_role = ApplicationRoleFactory.create( + application=app, status=ApplicationRoleStatus.ACTIVE, cloud_id="123", + ) + env = EnvironmentFactory.create(application=app, cloud_id="123") + env_role = EnvironmentRoleFactory.create( + environment=env, application_role=app_role, cloud_id=None + ) - csp = Mock() - result = UserRoleCSPResult(id="a-cloud-id") - csp.create_user_role = MagicMock(return_value=result) - do_create_environment_role(csp, environment_role_id=env_role.id) + csp = Mock() + result = UserRoleCSPResult(id="a-cloud-id") + csp.create_user_role = MagicMock(return_value=result) + do_create_environment_role(csp, environment_role_id=env_role.id) - assert env_role.cloud_id == "a-cloud-id" + assert env_role.cloud_id == "a-cloud-id" + + def test_sends_email(self, monkeypatch): + send_mail = Mock() + monkeypatch.setattr("atst.jobs.send_mail", send_mail) + + portfolio = PortfolioFactory.create(csp_data={"tenant_id": "123"}) + app = ApplicationFactory.create(portfolio=portfolio) + app_role = ApplicationRoleFactory.create( + application=app, status=ApplicationRoleStatus.ACTIVE, cloud_id="123", + ) + env = EnvironmentFactory.create(application=app, cloud_id="123") + env_role = EnvironmentRoleFactory.create( + environment=env, application_role=app_role, cloud_id=None + ) + + csp = Mock() + result = UserRoleCSPResult(id="a-cloud-id") + csp.create_user_role = MagicMock(return_value=result) + do_create_environment_role(csp, environment_role_id=env_role.id) + assert send_mail.call_count == 1 class TestSendTaskOrderFiles: diff --git a/translations.yaml b/translations.yaml index bc4e1479..0342df3f 100644 --- a/translations.yaml +++ b/translations.yaml @@ -86,7 +86,10 @@ email: app_role_created: subject: Application Role Created body: "Your application role has been created.\nVisit {url}, and use your username, {username}, to log in." - portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio." + azure_account_update: + subject: Azure account update + body: "There has been an update to your Azure account. \nVisit {url}, and use your username, {username}, to log in." + portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio" portfolio_ready: subject: Portfolio Provisioned body: "Your portfolio has been provisioned.\nVisit {password_reset_address}, and use your username, {username}, to create a password." From da0141a390652a312d6053d688952563fd1dd07e Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 18 Feb 2020 13:53:15 -0500 Subject: [PATCH 2/4] Refactor tests --- tests/test_jobs.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 8fc7df12..ea781c1b 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -400,41 +400,32 @@ def test_dispatch_create_environment_role(monkeypatch): class TestCreateEnvironmentRole: - def test_success(self): + @pytest.fixture + def env_role(self): portfolio = PortfolioFactory.create(csp_data={"tenant_id": "123"}) app = ApplicationFactory.create(portfolio=portfolio) app_role = ApplicationRoleFactory.create( application=app, status=ApplicationRoleStatus.ACTIVE, cloud_id="123", ) env = EnvironmentFactory.create(application=app, cloud_id="123") - env_role = EnvironmentRoleFactory.create( + return EnvironmentRoleFactory.create( environment=env, application_role=app_role, cloud_id=None ) + @pytest.fixture + def csp(self): csp = Mock() result = UserRoleCSPResult(id="a-cloud-id") csp.create_user_role = MagicMock(return_value=result) - do_create_environment_role(csp, environment_role_id=env_role.id) + return csp + def test_success(self, env_role, csp): + do_create_environment_role(csp, environment_role_id=env_role.id) assert env_role.cloud_id == "a-cloud-id" - def test_sends_email(self, monkeypatch): + def test_sends_email(self, monkeypatch, env_role, csp): send_mail = Mock() monkeypatch.setattr("atst.jobs.send_mail", send_mail) - - portfolio = PortfolioFactory.create(csp_data={"tenant_id": "123"}) - app = ApplicationFactory.create(portfolio=portfolio) - app_role = ApplicationRoleFactory.create( - application=app, status=ApplicationRoleStatus.ACTIVE, cloud_id="123", - ) - env = EnvironmentFactory.create(application=app, cloud_id="123") - env_role = EnvironmentRoleFactory.create( - environment=env, application_role=app_role, cloud_id=None - ) - - csp = Mock() - result = UserRoleCSPResult(id="a-cloud-id") - csp.create_user_role = MagicMock(return_value=result) do_create_environment_role(csp, environment_role_id=env_role.id) assert send_mail.call_count == 1 From 7a790b4b7009020af2a1876c7a690836daeea90e Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Fri, 21 Feb 2020 11:36:22 -0500 Subject: [PATCH 3/4] move creating the users principal name and mail nickname into a util file. Fix typo. --- atst/domain/csp/cloud/__init__.py | 4 ++++ atst/domain/csp/cloud/models.py | 9 ++++++--- atst/domain/csp/cloud/utils.py | 10 ++++++++++ atst/jobs.py | 13 +++++++------ atst/models/portfolio.py | 10 ++++++---- 5 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 atst/domain/csp/cloud/utils.py diff --git a/atst/domain/csp/cloud/__init__.py b/atst/domain/csp/cloud/__init__.py index 99128d9c..c3cc5dc0 100644 --- a/atst/domain/csp/cloud/__init__.py +++ b/atst/domain/csp/cloud/__init__.py @@ -1,3 +1,7 @@ from .azure_cloud_provider import AzureCloudProvider from .cloud_provider_interface import CloudProviderInterface from .mock_cloud_provider import MockCloudProvider +from .utils import ( + generate_mail_nickname, + generate_user_principal_name, +) diff --git a/atst/domain/csp/cloud/models.py b/atst/domain/csp/cloud/models.py index 27f2c9c7..4a80084b 100644 --- a/atst/domain/csp/cloud/models.py +++ b/atst/domain/csp/cloud/models.py @@ -4,9 +4,12 @@ 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 .utils import ( + generate_mail_nickname, + generate_user_principal_name, +) from atst.utils import snake_to_camel @@ -527,11 +530,11 @@ class UserMixin(BaseModel): @property def user_principal_name(self): - return f"{self.mail_nickname}@{self.tenant_host_name}.{app.config.get('OFFICE_365_DOMAIN')}" + return generate_user_principal_name(self.display_name, self.tenant_host_name) @property def mail_nickname(self): - return self.display_name.replace(" ", ".").lower() + return generate_mail_nickname(self.display_name) @validator("password", pre=True, always=True) def supply_password_default(cls, password): diff --git a/atst/domain/csp/cloud/utils.py b/atst/domain/csp/cloud/utils.py new file mode 100644 index 00000000..883e69d2 --- /dev/null +++ b/atst/domain/csp/cloud/utils.py @@ -0,0 +1,10 @@ +from flask import current_app as app + + +def generate_user_principal_name(name, domain_name): + mail_name = generate_mail_nickname(name) + return f"{mail_name}@{domain_name}.{app.config.get('OFFICE_365_DOMAIN')}" + + +def generate_mail_nickname(name): + return name.replace(" ", ".").lower() diff --git a/atst/jobs.py b/atst/jobs.py index 1dbff894..dcfb4bf3 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -6,7 +6,7 @@ from azure.core.exceptions import AzureError from atst.database import db from atst.domain.application_roles import ApplicationRoles from atst.domain.applications import Applications -from atst.domain.csp.cloud import CloudProviderInterface +from atst.domain.csp.cloud import CloudProviderInterface, generate_user_principal_name from atst.domain.csp.cloud.exceptions import GeneralCSPException from atst.domain.csp.cloud.models import ( ApplicationCSPPayload, @@ -177,9 +177,10 @@ def do_create_environment_role(csp: CloudProviderInterface, environment_role_id= env_role.cloud_id = result.id db.session.add(env_role) db.session.commit() + user = env_role.application_role.user - mail_name = user.full_name.replace(" ", ".").lower() - username = f"{mail_name}@{csp_details.get('tennant_id')}.{app.config.get('OFFICE_365_DOMAIN')}" + domain_name = csp_details.get("domain_name") + username = generate_user_principal_name(user.full_name, domain_name,) send_mail( recipients=[user.email], subject=translate("email.azure_account_update.subject"), @@ -189,7 +190,7 @@ def do_create_environment_role(csp: CloudProviderInterface, environment_role_id= ), ) app.logger.info( - f"Notification email sent for enivornment role creation. User id: {user.id}" + f"Notification email sent for environment role creation. User id: {user.id}" ) @@ -208,7 +209,7 @@ 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") - + username = generate_user_principal_name(user_id, domain_name) send_mail( recipients=[ppoc_email], subject=translate("email.portfolio_ready.subject"), @@ -216,7 +217,7 @@ def send_PPOC_email(portfolio_dict): "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')}", + "username": username, }, ), ) diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py index bd430ed3..0243b215 100644 --- a/atst/models/portfolio.py +++ b/atst/models/portfolio.py @@ -12,10 +12,10 @@ from sqlalchemy_json import NestedMutableJson from atst.database import db import atst.models.mixins as mixins import atst.models.types as types +from atst.domain.csp.cloud import generate_mail_nickname from atst.domain.permission_sets import PermissionSets from atst.models.base import Base -from atst.models.portfolio_role import PortfolioRole -from atst.models.portfolio_role import Status as PortfolioRoleStatus +from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus from atst.utils import first_or_none @@ -188,7 +188,7 @@ class Portfolio( @property def domain_name(self): """ - CSP domain name associated with portfolio. + CSP domain name associated with portfolio. If a domain name is not set, generate one. """ domain_name = re.sub("[^0-9a-zA-Z]+", "", self.name).lower() + "".join( @@ -205,7 +205,9 @@ class Portfolio( def to_dictionary(self): return { - "user_id": f"{self.owner.first_name[0]}{self.owner.last_name}".lower(), + "user_id": generate_mail_nickname( + f"{self.owner.first_name[0]}{self.owner.last_name}" + ), "password": "", "domain_name": self.domain_name, "first_name": self.owner.first_name, From 668aa89edf86fdeecf1da17432df44fe249d5d8a Mon Sep 17 00:00:00 2001 From: leigh-mil Date: Tue, 25 Feb 2020 13:39:36 -0500 Subject: [PATCH 4/4] fix circular import --- atst/domain/csp/cloud/__init__.py | 4 ---- atst/jobs.py | 3 ++- atst/models/portfolio.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/atst/domain/csp/cloud/__init__.py b/atst/domain/csp/cloud/__init__.py index c3cc5dc0..99128d9c 100644 --- a/atst/domain/csp/cloud/__init__.py +++ b/atst/domain/csp/cloud/__init__.py @@ -1,7 +1,3 @@ from .azure_cloud_provider import AzureCloudProvider from .cloud_provider_interface import CloudProviderInterface from .mock_cloud_provider import MockCloudProvider -from .utils import ( - generate_mail_nickname, - generate_user_principal_name, -) diff --git a/atst/jobs.py b/atst/jobs.py index dcfb4bf3..b3e5643b 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -6,7 +6,8 @@ from azure.core.exceptions import AzureError from atst.database import db from atst.domain.application_roles import ApplicationRoles from atst.domain.applications import Applications -from atst.domain.csp.cloud import CloudProviderInterface, generate_user_principal_name +from atst.domain.csp.cloud import CloudProviderInterface +from atst.domain.csp.cloud.utils import generate_user_principal_name from atst.domain.csp.cloud.exceptions import GeneralCSPException from atst.domain.csp.cloud.models import ( ApplicationCSPPayload, diff --git a/atst/models/portfolio.py b/atst/models/portfolio.py index 0243b215..fdf5613b 100644 --- a/atst/models/portfolio.py +++ b/atst/models/portfolio.py @@ -12,7 +12,7 @@ from sqlalchemy_json import NestedMutableJson from atst.database import db import atst.models.mixins as mixins import atst.models.types as types -from atst.domain.csp.cloud import generate_mail_nickname +from atst.domain.csp.cloud.utils import generate_mail_nickname from atst.domain.permission_sets import PermissionSets from atst.models.base import Base from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus