Adds a method for creating an Active Directory user.
This method is added to the Azure cloud interface. We need to set the AAD user's alternate email, which is a subsequent PATCH call to the API. These two calls are handled with a single interface method and payload because ATAT would never create a user without an associated email. This commit also: - Expands internal method for getting principal tokens so that it can be scoped to different resources. - Retains the tenant domain name in the portfolios.csp_data column because ATAT needs that information for provisioning users via API.
This commit is contained in:
parent
cc28f53999
commit
b1c6dd5ad0
@ -6,7 +6,7 @@ from uuid import uuid4
|
|||||||
from atst.utils import sha256_hex
|
from atst.utils import sha256_hex
|
||||||
|
|
||||||
from .cloud_provider_interface import CloudProviderInterface
|
from .cloud_provider_interface import CloudProviderInterface
|
||||||
from .exceptions import AuthenticationException
|
from .exceptions import AuthenticationException, UserProvisioningException
|
||||||
from .models import (
|
from .models import (
|
||||||
SubscriptionCreationCSPPayload,
|
SubscriptionCreationCSPPayload,
|
||||||
SubscriptionCreationCSPResult,
|
SubscriptionCreationCSPResult,
|
||||||
@ -48,6 +48,8 @@ from .models import (
|
|||||||
TenantPrincipalCSPResult,
|
TenantPrincipalCSPResult,
|
||||||
TenantPrincipalOwnershipCSPPayload,
|
TenantPrincipalOwnershipCSPPayload,
|
||||||
TenantPrincipalOwnershipCSPResult,
|
TenantPrincipalOwnershipCSPResult,
|
||||||
|
UserCSPPayload,
|
||||||
|
UserCSPResult,
|
||||||
)
|
)
|
||||||
from .policy import AzurePolicyManager
|
from .policy import AzurePolicyManager
|
||||||
|
|
||||||
@ -191,6 +193,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
|
|
||||||
def create_application(self, payload: ApplicationCSPPayload):
|
def create_application(self, payload: ApplicationCSPPayload):
|
||||||
creds = self._source_creds(payload.tenant_id)
|
creds = self._source_creds(payload.tenant_id)
|
||||||
|
# TODO: these should be tenant_sp_client_id, etc
|
||||||
credentials = self._get_credential_obj(
|
credentials = self._get_credential_obj(
|
||||||
{
|
{
|
||||||
"client_id": creds.root_sp_client_id,
|
"client_id": creds.root_sp_client_id,
|
||||||
@ -310,7 +313,9 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
tenant_admin_password=payload.password,
|
tenant_admin_password=payload.password,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return self._ok(TenantCSPResult(**result_dict))
|
return self._ok(
|
||||||
|
TenantCSPResult(domain_name=payload.domain_name, **result_dict)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return self._error(result.json())
|
return self._error(result.json())
|
||||||
|
|
||||||
@ -850,6 +855,80 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
|
|
||||||
return service_principal
|
return service_principal
|
||||||
|
|
||||||
|
def create_user(self, payload: UserCSPPayload) -> UserCSPResult:
|
||||||
|
"""Create a user in an Azure Active Directory instance.
|
||||||
|
Unlike most of the methods on this interface, this requires
|
||||||
|
two API calls: one POST to create the user and one PATCH to
|
||||||
|
set the alternate email address. The email address cannot
|
||||||
|
be set on the first API call. The email address is
|
||||||
|
necessary so that users can do Self-Service Password
|
||||||
|
Recovery.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
payload {UserCSPPayload} -- a payload object with the
|
||||||
|
data necessary for both calls
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UserCSPResult -- a result object containing the AAD ID.
|
||||||
|
"""
|
||||||
|
graph_token = self._get_tenant_principal_token(
|
||||||
|
payload.tenant_id, resource=self.graph_resource
|
||||||
|
)
|
||||||
|
if graph_token is None:
|
||||||
|
raise AuthenticationException(
|
||||||
|
"Could not resolve graph token for tenant admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self._create_active_directory_user(graph_token, payload)
|
||||||
|
self._update_active_directory_user_email(graph_token, result.id, payload)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _create_active_directory_user(self, graph_token, payload: UserCSPPayload):
|
||||||
|
request_body = {
|
||||||
|
"accountEnabled": True,
|
||||||
|
"displayName": payload.display_name,
|
||||||
|
"mailNickname": payload.mail_nickname,
|
||||||
|
"userPrincipalName": payload.user_principal_name,
|
||||||
|
"passwordProfile": {
|
||||||
|
"forceChangePasswordNextSignIn": True,
|
||||||
|
"password": payload.password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header = {
|
||||||
|
"Authorization": f"Bearer {graph_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{self.graph_resource}v1.0/users"
|
||||||
|
|
||||||
|
response = self.sdk.requests.post(url, headers=auth_header, json=request_body)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
return UserCSPResult(**response.json())
|
||||||
|
else:
|
||||||
|
raise UserProvisioningException(f"Failed to create user: {response.json()}")
|
||||||
|
|
||||||
|
def _update_active_directory_user_email(
|
||||||
|
self, graph_token, user_id, payload: UserCSPPayload
|
||||||
|
):
|
||||||
|
request_body = {"otherMails": [payload.email]}
|
||||||
|
|
||||||
|
auth_header = {
|
||||||
|
"Authorization": f"Bearer {graph_token}",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{self.graph_resource}v1.0/users/{user_id}"
|
||||||
|
|
||||||
|
response = self.sdk.requests.patch(url, headers=auth_header, json=request_body)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise UserProvisioningException(
|
||||||
|
f"Failed update user email: {response.json()}"
|
||||||
|
)
|
||||||
|
|
||||||
def _extract_subscription_id(self, subscription_url):
|
def _extract_subscription_id(self, subscription_url):
|
||||||
sub_id_match = SUBSCRIPTION_ID_REGEX.match(subscription_url)
|
sub_id_match = SUBSCRIPTION_ID_REGEX.match(subscription_url)
|
||||||
|
|
||||||
@ -871,14 +950,15 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
creds.root_tenant_id, creds.root_sp_client_id, creds.root_sp_key
|
creds.root_tenant_id, creds.root_sp_client_id, creds.root_sp_key
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_sp_token(self, tenant_id, client_id, secret_key):
|
def _get_sp_token(self, tenant_id, client_id, secret_key, resource=None):
|
||||||
context = self.sdk.adal.AuthenticationContext(
|
context = self.sdk.adal.AuthenticationContext(
|
||||||
f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
|
f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
resource = resource or self.sdk.cloud.endpoints.resource_manager
|
||||||
# TODO: handle failure states here
|
# TODO: handle failure states here
|
||||||
token_response = context.acquire_token_with_client_credentials(
|
token_response = context.acquire_token_with_client_credentials(
|
||||||
self.sdk.cloud.endpoints.resource_manager, client_id, secret_key
|
resource, client_id, secret_key
|
||||||
)
|
)
|
||||||
|
|
||||||
return token_response.get("accessToken", None)
|
return token_response.get("accessToken", None)
|
||||||
@ -939,10 +1019,13 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
"tenant_id": self.tenant_id,
|
"tenant_id": self.tenant_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_tenant_principal_token(self, tenant_id):
|
def _get_tenant_principal_token(self, tenant_id, resource=None):
|
||||||
creds = self._source_creds(tenant_id)
|
creds = self._source_creds(tenant_id)
|
||||||
return self._get_sp_token(
|
return self._get_sp_token(
|
||||||
creds.tenant_id, creds.tenant_sp_client_id, creds.tenant_sp_key
|
creds.tenant_id,
|
||||||
|
creds.tenant_sp_client_id,
|
||||||
|
creds.tenant_sp_key,
|
||||||
|
resource=resource,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_elevated_management_token(self, tenant_id):
|
def _get_elevated_management_token(self, tenant_id):
|
||||||
|
@ -88,17 +88,6 @@ class UserProvisioningException(GeneralCSPException):
|
|||||||
"""Failed to provision a user
|
"""Failed to provision a user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, env_identifier, user_identifier, reason):
|
|
||||||
self.env_identifier = env_identifier
|
|
||||||
self.user_identifier = user_identifier
|
|
||||||
self.reason = reason
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "Failed to create user {} for environment {}: {}".format(
|
|
||||||
self.user_identifier, self.env_identifier, self.reason
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserRemovalException(GeneralCSPException):
|
class UserRemovalException(GeneralCSPException):
|
||||||
"""Failed to remove a user
|
"""Failed to remove a user
|
||||||
|
@ -175,6 +175,7 @@ class MockCloudProvider(CloudProviderInterface):
|
|||||||
"tenant_id": "",
|
"tenant_id": "",
|
||||||
"user_id": "",
|
"user_id": "",
|
||||||
"user_object_id": "",
|
"user_object_id": "",
|
||||||
|
"domain_name": "",
|
||||||
"tenant_admin_username": "test",
|
"tenant_admin_username": "test",
|
||||||
"tenant_admin_password": "test",
|
"tenant_admin_password": "test",
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ class TenantCSPResult(AliasModel):
|
|||||||
user_id: str
|
user_id: str
|
||||||
tenant_id: str
|
tenant_id: str
|
||||||
user_object_id: str
|
user_object_id: str
|
||||||
|
domain_name: str
|
||||||
|
|
||||||
tenant_admin_username: Optional[str]
|
tenant_admin_username: Optional[str]
|
||||||
tenant_admin_password: Optional[str]
|
tenant_admin_password: Optional[str]
|
||||||
@ -474,3 +475,25 @@ class ProductPurchaseVerificationCSPPayload(BaseCSPPayload):
|
|||||||
|
|
||||||
class ProductPurchaseVerificationCSPResult(AliasModel):
|
class ProductPurchaseVerificationCSPResult(AliasModel):
|
||||||
premium_purchase_date: str
|
premium_purchase_date: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserCSPPayload(BaseCSPPayload):
|
||||||
|
# userPrincipalName must be username + tenant
|
||||||
|
# display name should be full name
|
||||||
|
# mail nickname should be... email address?
|
||||||
|
display_name: str
|
||||||
|
tenant_host_name: str
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_principal_name(self):
|
||||||
|
return f"{self.mail_nickname}@{self.tenant_host_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mail_nickname(self):
|
||||||
|
return self.display_name.replace(" ", ".").lower()
|
||||||
|
|
||||||
|
|
||||||
|
class UserCSPResult(AliasModel):
|
||||||
|
id: str
|
||||||
|
Loading…
x
Reference in New Issue
Block a user