Celery wrapper for creating a user.

This commit is contained in:
dandds 2020-02-02 13:58:41 -05:00
parent b1c6dd5ad0
commit 6b8d9d1d65
8 changed files with 154 additions and 105 deletions

View File

@ -65,6 +65,15 @@ class ApplicationRoles(object):
except NoResultFound: except NoResultFound:
raise NotFoundError("application_role") raise NotFoundError("application_role")
@classmethod
def get_many(cls, ids):
return (
db.session.query(ApplicationRole)
.filter(ApplicationRole.id.in_(ids))
.filter(ApplicationRole.status != ApplicationRoleStatus.DISABLED)
.all()
)
@classmethod @classmethod
def update_permission_sets(cls, application_role, new_perm_sets_names): def update_permission_sets(cls, application_role, new_perm_sets_names):
application_role.permission_sets = ApplicationRoles._permission_sets_for_names( application_role.permission_sets = ApplicationRoles._permission_sets_for_names(

View File

@ -51,6 +51,8 @@ from .models import (
TenantPrincipalCSPResult, TenantPrincipalCSPResult,
TenantPrincipalOwnershipCSPPayload, TenantPrincipalOwnershipCSPPayload,
TenantPrincipalOwnershipCSPResult, TenantPrincipalOwnershipCSPResult,
UserCSPPayload,
UserCSPResult,
) )
@ -475,6 +477,11 @@ class MockCloudProvider(CloudProviderInterface):
id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}" id=f"{AZURE_MGMNT_PATH}{payload.management_group_name}"
) )
def create_user(self, payload: UserCSPPayload):
self._maybe_raise(self.UNAUTHORIZED_RATE, GeneralCSPException)
return UserCSPResult(id=str(uuid4()))
def get_credentials(self, scope="portfolio", tenant_id=None): def get_credentials(self, scope="portfolio", tenant_id=None):
return self.root_creds() return self.root_creds()

View File

@ -1,6 +1,7 @@
from secrets import token_urlsafe
from typing import Dict, List, Optional from typing import Dict, List, Optional
import re
from uuid import uuid4 from uuid import uuid4
import re
from pydantic import BaseModel, validator, root_validator from pydantic import BaseModel, validator, root_validator
@ -478,13 +479,10 @@ class ProductPurchaseVerificationCSPResult(AliasModel):
class UserCSPPayload(BaseCSPPayload): class UserCSPPayload(BaseCSPPayload):
# userPrincipalName must be username + tenant
# display name should be full name
# mail nickname should be... email address?
display_name: str display_name: str
tenant_host_name: str tenant_host_name: str
email: str email: str
password: str password: Optional[str]
@property @property
def user_principal_name(self): def user_principal_name(self):
@ -494,6 +492,10 @@ class UserCSPPayload(BaseCSPPayload):
def mail_nickname(self): def mail_nickname(self):
return self.display_name.replace(" ", ".").lower() return self.display_name.replace(" ", ".").lower()
@validator("password", pre=True, always=True)
def supply_password_default(cls, password):
return password or token_urlsafe(16)
class UserCSPResult(AliasModel): class UserCSPResult(AliasModel):
id: str id: str

View File

@ -3,16 +3,16 @@ import pendulum
from atst.database import db from atst.database import db
from atst.queue import celery from atst.queue import celery
from atst.models import EnvironmentRole, JobFailure from atst.models import JobFailure
from atst.domain.csp.cloud.exceptions import GeneralCSPException from atst.domain.csp.cloud.exceptions import GeneralCSPException
from atst.domain.csp.cloud import CloudProviderInterface from atst.domain.csp.cloud import CloudProviderInterface
from atst.domain.applications import Applications from atst.domain.applications import Applications
from atst.domain.environments import Environments from atst.domain.environments import Environments
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.domain.environment_roles import EnvironmentRoles from atst.domain.application_roles import ApplicationRoles
from atst.models.utils import claim_for_update from atst.models.utils import claim_for_update, claim_many_for_update
from atst.utils.localization import translate from atst.utils.localization import translate
from atst.domain.csp.cloud.models import ApplicationCSPPayload from atst.domain.csp.cloud.models import ApplicationCSPPayload, UserCSPPayload
class RecordFailure(celery.Task): class RecordFailure(celery.Task):
@ -75,6 +75,34 @@ def do_create_application(csp: CloudProviderInterface, application_id=None):
db.session.commit() db.session.commit()
def do_create_user(csp: CloudProviderInterface, application_role_ids=None):
if not application_role_ids:
return
app_roles = ApplicationRoles.get_many(application_role_ids)
with claim_many_for_update(app_roles) as app_roles:
if any([ar.cloud_id for ar in app_roles]):
return
csp_details = app_roles[0].application.portfolio.csp_data
user = app_roles[0].user
payload = UserCSPPayload(
tenant_id=csp_details.get("tenant_id"),
tenant_host_name=csp_details.get("domain_name"),
display_name=user.full_name,
email=user.email,
)
result = csp.create_user(payload)
for app_role in app_roles:
app_role.cloud_id = result.id
db.session.add(app_role)
db.session.commit()
def do_create_environment(csp: CloudProviderInterface, environment_id=None): def do_create_environment(csp: CloudProviderInterface, environment_id=None):
environment = Environments.get(environment_id) environment = Environments.get(environment_id)
@ -128,21 +156,6 @@ def render_email(template_path, context):
return app.jinja_env.get_template(template_path).render(context) return app.jinja_env.get_template(template_path).render(context)
def do_provision_user(csp: CloudProviderInterface, environment_role_id=None):
environment_role = EnvironmentRoles.get_by_id(environment_role_id)
with claim_for_update(environment_role) as environment_role:
credentials = environment_role.environment.csp_credentials
csp_user_id = csp.create_or_update_user(
credentials, environment_role, environment_role.role
)
environment_role.csp_user_id = csp_user_id
environment_role.status = EnvironmentRole.Status.COMPLETED
db.session.add(environment_role)
db.session.commit()
def do_work(fn, task, csp, **kwargs): def do_work(fn, task, csp, **kwargs):
try: try:
fn(csp, **kwargs) fn(csp, **kwargs)
@ -166,6 +179,13 @@ def create_application(self, application_id=None):
do_work(do_create_application, self, app.csp.cloud, application_id=application_id) do_work(do_create_application, self, app.csp.cloud, application_id=application_id)
@celery.task(bind=True, base=RecordFailure)
def create_user(self, application_role_ids=None):
do_work(
do_create_user, self, app.csp.cloud, application_role_ids=application_role_ids
)
@celery.task(bind=True, base=RecordFailure) @celery.task(bind=True, base=RecordFailure)
def create_environment(self, environment_id=None): def create_environment(self, environment_id=None):
do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id) do_work(do_create_environment, self, app.csp.cloud, environment_id=environment_id)
@ -178,13 +198,6 @@ def create_atat_admin_user(self, environment_id=None):
) )
@celery.task(bind=True)
def provision_user(self, environment_role_id=None):
do_work(
do_provision_user, self, app.csp.cloud, environment_role_id=environment_role_id
)
@celery.task(bind=True) @celery.task(bind=True)
def dispatch_provision_portfolio(self): def dispatch_provision_portfolio(self):
""" """
@ -200,6 +213,12 @@ def dispatch_create_application(self):
create_application.delay(application_id=application_id) create_application.delay(application_id=application_id)
@celery.task(bind=True)
def dispatch_create_user(self):
for application_role_ids in ApplicationRoles.get_pending_creation():
create_user.delay(application_role_ids=application_role_ids)
@celery.task(bind=True) @celery.task(bind=True)
def dispatch_create_environment(self): def dispatch_create_environment(self):
for environment_id in Environments.get_environments_pending_creation( for environment_id in Environments.get_environments_pending_creation(
@ -214,11 +233,3 @@ def dispatch_create_atat_admin_user(self):
pendulum.now() pendulum.now()
): ):
create_atat_admin_user.delay(environment_id=environment_id) create_atat_admin_user.delay(environment_id=environment_id)
@celery.task(bind=True)
def dispatch_provision_user(self):
for (
environment_role_id
) in EnvironmentRoles.get_environment_roles_pending_creation():
provision_user.delay(environment_role_id=environment_role_id)

View File

@ -23,8 +23,8 @@ def update_celery(celery, app):
"task": "atst.jobs.dispatch_create_atat_admin_user", "task": "atst.jobs.dispatch_create_atat_admin_user",
"schedule": 60, "schedule": 60,
}, },
"beat-dispatch_provision_user": { "beat-dispatch_create_user": {
"task": "atst.jobs.dispatch_provision_user", "task": "atst.jobs.dispatch_create_user",
"schedule": 60, "schedule": 60,
}, },
} }

View File

@ -7,6 +7,7 @@ from atst.domain.csp.cloud.models import (
KeyVaultCredentials, KeyVaultCredentials,
ManagementGroupCSPPayload, ManagementGroupCSPPayload,
ManagementGroupCSPResponse, ManagementGroupCSPResponse,
UserCSPPayload,
) )
@ -97,3 +98,26 @@ def test_KeyVaultCredentials_enforce_root_creds():
assert KeyVaultCredentials( assert KeyVaultCredentials(
root_tenant_id="an id", root_sp_client_id="C3PO", root_sp_key="beep boop" root_tenant_id="an id", root_sp_client_id="C3PO", root_sp_key="beep boop"
) )
user_payload = {
"tenant_id": "123",
"display_name": "Han Solo",
"tenant_host_name": "rebelalliance.com",
"email": "han@moseisley.cantina",
}
def test_UserCSPPayload_mail_nickname():
payload = UserCSPPayload(**user_payload)
assert payload.mail_nickname == f"han.solo"
def test_UserCSPPayload_user_principal_name():
payload = UserCSPPayload(**user_payload)
assert payload.user_principal_name == f"han.solo@rebelalliance.com"
def test_UserCSPPayload_password():
payload = UserCSPPayload(**user_payload)
assert payload.password

View File

@ -153,3 +153,12 @@ def test_get_pending_creation():
expected_ids = [[role_one.id, role_two.id], [role_three.id], [role_four.id]] expected_ids = [[role_one.id, role_two.id], [role_three.id], [role_four.id]]
# Sort them to produce the same order. # Sort them to produce the same order.
assert sorted(app_ids) == sorted(expected_ids) assert sorted(app_ids) == sorted(expected_ids)
def test_get_many():
ar1 = ApplicationRoleFactory.create()
ar2 = ApplicationRoleFactory.create()
ApplicationRoleFactory.create()
result = ApplicationRoles.get_many([ar1.id, ar2.id])
assert result == [ar1, ar2]

View File

@ -5,16 +5,17 @@ from unittest.mock import Mock
from atst.domain.csp.cloud import MockCloudProvider from atst.domain.csp.cloud import MockCloudProvider
from atst.domain.portfolios import Portfolios from atst.domain.portfolios import Portfolios
from atst.models import ApplicationRoleStatus
from atst.jobs import ( from atst.jobs import (
RecordFailure, RecordFailure,
dispatch_create_environment, dispatch_create_environment,
dispatch_create_application, dispatch_create_application,
dispatch_create_user,
dispatch_create_atat_admin_user, dispatch_create_atat_admin_user,
dispatch_provision_portfolio, dispatch_provision_portfolio,
dispatch_provision_user,
create_environment, create_environment,
do_provision_user, do_create_user,
do_provision_portfolio, do_provision_portfolio,
do_create_environment, do_create_environment,
do_create_application, do_create_application,
@ -27,6 +28,7 @@ from tests.factories import (
PortfolioStateMachineFactory, PortfolioStateMachineFactory,
ApplicationFactory, ApplicationFactory,
ApplicationRoleFactory, ApplicationRoleFactory,
UserFactory,
) )
from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure from atst.models import CSPRole, EnvironmentRole, ApplicationRoleStatus, JobFailure
@ -123,6 +125,30 @@ 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):
portfolio = PortfolioFactory.create(
csp_data={
"tenant_id": str(uuid4()),
"domain_name": "rebelalliance.onmicrosoft.com",
}
)
application = ApplicationFactory.create(portfolio=portfolio, cloud_id="321")
user = UserFactory.create(
first_name="Han", last_name="Solo", email="han@example.com"
)
app_role = ApplicationRoleFactory.create(
application=application,
user=user,
status=ApplicationRoleStatus.ACTIVE,
cloud_id=None,
)
do_create_user(csp, [app_role.id])
session.refresh(app_role)
assert app_role.cloud_id
def test_create_atat_admin_user(csp, session): def test_create_atat_admin_user(csp, session):
environment = EnvironmentFactory.create(cloud_id="something") environment = EnvironmentFactory.create(cloud_id="something")
do_create_atat_admin_user(csp, environment.id) do_create_atat_admin_user(csp, environment.id)
@ -178,6 +204,29 @@ def test_dispatch_create_application(monkeypatch):
mock.delay.assert_called_once_with(application_id=app.id) mock.delay.assert_called_once_with(application_id=app.id)
def test_dispatch_create_user(monkeypatch):
application = ApplicationFactory.create(cloud_id="123")
user = UserFactory.create(
first_name="Han", last_name="Solo", email="han@example.com"
)
app_role = ApplicationRoleFactory.create(
application=application,
user=user,
status=ApplicationRoleStatus.ACTIVE,
cloud_id=None,
)
mock = Mock()
monkeypatch.setattr("atst.jobs.create_user", mock)
# When dispatch_create_user is called
dispatch_create_user.run()
# It should cause the create_user task to be called once
# with the application id
mock.delay.assert_called_once_with(application_role_ids=[app_role.id])
def test_dispatch_create_atat_admin_user(session, monkeypatch): def test_dispatch_create_atat_admin_user(session, monkeypatch):
portfolio = PortfolioFactory.create( portfolio = PortfolioFactory.create(
applications=[ applications=[
@ -237,68 +286,6 @@ def test_create_environment_no_dupes(session, celery_app, celery_worker):
assert environment.claimed_until == None assert environment.claimed_until == None
def test_dispatch_provision_user(csp, session, celery_app, celery_worker, monkeypatch):
# Given that I have four environment roles:
# (A) one of which has a completed status
# (B) one of which has an environment that has not been provisioned
# (C) one of which is pending, has a provisioned environment but an inactive application role
# (D) one of which is pending, has a provisioned environment and has an active application role
provisioned_environment = EnvironmentFactory.create(
cloud_id="cloud_id", root_user_info={}
)
unprovisioned_environment = EnvironmentFactory.create()
_er_a = EnvironmentRoleFactory.create(
environment=provisioned_environment, status=EnvironmentRole.Status.COMPLETED
)
_er_b = EnvironmentRoleFactory.create(
environment=unprovisioned_environment, status=EnvironmentRole.Status.PENDING
)
_er_c = EnvironmentRoleFactory.create(
environment=unprovisioned_environment,
status=EnvironmentRole.Status.PENDING,
application_role=ApplicationRoleFactory(status=ApplicationRoleStatus.PENDING),
)
er_d = EnvironmentRoleFactory.create(
environment=provisioned_environment,
status=EnvironmentRole.Status.PENDING,
application_role=ApplicationRoleFactory(status=ApplicationRoleStatus.ACTIVE),
)
mock = Mock()
monkeypatch.setattr("atst.jobs.provision_user", mock)
# When I dispatch the user provisioning task
dispatch_provision_user.run()
# I expect it to dispatch only one call, to EnvironmentRole D
mock.delay.assert_called_once_with(environment_role_id=er_d.id)
def test_do_provision_user(csp, session):
# Given that I have an EnvironmentRole with a provisioned environment
credentials = MockCloudProvider(())._auth_credentials
provisioned_environment = EnvironmentFactory.create(
cloud_id="cloud_id", root_user_info={"credentials": credentials}
)
environment_role = EnvironmentRoleFactory.create(
environment=provisioned_environment,
status=EnvironmentRole.Status.PENDING,
role="ADMIN",
)
# When I call the user provisoning task
do_provision_user(csp=csp, environment_role_id=environment_role.id)
session.refresh(environment_role)
# I expect that the CSP create_or_update_user method will be called
csp.create_or_update_user.assert_called_once_with(
credentials, environment_role, CSPRole.ADMIN
)
# I expect that the EnvironmentRole now has a csp_user_id
assert environment_role.csp_user_id
def test_dispatch_provision_portfolio( def test_dispatch_provision_portfolio(
csp, session, portfolio, celery_app, celery_worker, monkeypatch csp, session, portfolio, celery_app, celery_worker, monkeypatch
): ):