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 80585d3a..b3e5643b 100644 --- a/atst/jobs.py +++ b/atst/jobs.py @@ -7,6 +7,7 @@ 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.utils import generate_user_principal_name from atst.domain.csp.cloud.exceptions import GeneralCSPException from atst.domain.csp.cloud.models import ( ApplicationCSPPayload, @@ -177,7 +178,21 @@ 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 + 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"), + body=translate( + "email.azure_account_update.body", + {"url": app.config.get("AZURE_LOGIN_URL"), "username": username}, + ), + ) + app.logger.info( + f"Notification email sent for environment role creation. User id: {user.id}" + ) def render_email(template_path, context): @@ -195,7 +210,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"), @@ -203,7 +218,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..fdf5613b 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.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 -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, diff --git a/tests/test_jobs.py b/tests/test_jobs.py index eba01f9b..ea781c1b 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -399,23 +399,35 @@ 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: + @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") + return 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) + @pytest.fixture + def csp(self): + csp = Mock() + result = UserRoleCSPResult(id="a-cloud-id") + csp.create_user_role = MagicMock(return_value=result) + return csp - assert env_role.cloud_id == "a-cloud-id" + 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, env_role, csp): + send_mail = Mock() + monkeypatch.setattr("atst.jobs.send_mail", send_mail) + 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."