resolve merge conflict with staging
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import json
|
||||
import re
|
||||
from secrets import token_urlsafe
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
from uuid import uuid4
|
||||
|
||||
from atst.utils import sha256_hex
|
||||
|
||||
from .cloud_provider_interface import CloudProviderInterface
|
||||
from .exceptions import AuthenticationException
|
||||
from .models import (
|
||||
AdminRoleDefinitionCSPPayload,
|
||||
AdminRoleDefinitionCSPResult,
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
@@ -23,26 +27,36 @@ from .models import (
|
||||
ProductPurchaseCSPResult,
|
||||
ProductPurchaseVerificationCSPPayload,
|
||||
ProductPurchaseVerificationCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
TaskOrderBillingVerificationCSPResult,
|
||||
TenantAdminOwnershipCSPPayload,
|
||||
TenantAdminOwnershipCSPResult,
|
||||
TenantCSPPayload,
|
||||
TenantCSPResult,
|
||||
TenantPrincipalAppCSPPayload,
|
||||
TenantPrincipalAppCSPResult,
|
||||
TenantPrincipalCredentialCSPPayload,
|
||||
TenantPrincipalCredentialCSPResult,
|
||||
TenantPrincipalCSPPayload,
|
||||
TenantPrincipalCSPResult,
|
||||
TenantPrincipalOwnershipCSPPayload,
|
||||
TenantPrincipalOwnershipCSPResult,
|
||||
)
|
||||
from .policy import AzurePolicyManager
|
||||
from atst.utils import sha256_hex
|
||||
|
||||
AZURE_ENVIRONMENT = "AZURE_PUBLIC_CLOUD" # TBD
|
||||
AZURE_SKU_ID = "?" # probably a static sku specific to ATAT/JEDI
|
||||
SUBSCRIPTION_ID_REGEX = re.compile(
|
||||
"subscriptions\/([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})",
|
||||
re.I,
|
||||
)
|
||||
|
||||
# This needs to be a fully pathed role definition identifier, not just a UUID
|
||||
# TODO: Extract these from sdk msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
AZURE_SKU_ID = "0001" # probably a static sku specific to ATAT/JEDI
|
||||
REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00000000-0000-4000-8000-000000000000"
|
||||
AZURE_MANAGEMENT_API = "https://management.azure.com"
|
||||
|
||||
|
||||
class AzureSDKProvider(object):
|
||||
@@ -54,8 +68,9 @@ class AzureSDKProvider(object):
|
||||
import azure.identity as identity
|
||||
from azure.keyvault import secrets
|
||||
from azure.core import exceptions
|
||||
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
from msrestazure.azure_cloud import (
|
||||
AZURE_PUBLIC_CLOUD,
|
||||
) # TODO: choose cloud type from config
|
||||
import adal
|
||||
import requests
|
||||
|
||||
@@ -70,7 +85,6 @@ class AzureSDKProvider(object):
|
||||
self.exceptions = exceptions
|
||||
self.secrets = secrets
|
||||
self.requests = requests
|
||||
# may change to a JEDI cloud
|
||||
self.cloud = AZURE_PUBLIC_CLOUD
|
||||
|
||||
|
||||
@@ -82,6 +96,9 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
self.secret_key = config["AZURE_SECRET_KEY"]
|
||||
self.tenant_id = config["AZURE_TENANT_ID"]
|
||||
self.vault_url = config["AZURE_VAULT_URL"]
|
||||
self.ps_client_id = config["POWERSHELL_CLIENT_ID"]
|
||||
self.owner_role_def_id = config["AZURE_OWNER_ROLE_DEF_ID"]
|
||||
self.graph_resource = config["AZURE_GRAPH_RESOURCE"]
|
||||
|
||||
if azure_sdk_provider is None:
|
||||
self.sdk = AzureSDKProvider()
|
||||
@@ -91,7 +108,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
|
||||
|
||||
def set_secret(self, secret_key, secret_value):
|
||||
credential = self._get_client_secret_credential_obj({})
|
||||
credential = self._get_client_secret_credential_obj()
|
||||
secret_client = self.sdk.secrets.SecretClient(
|
||||
vault_url=self.vault_url, credential=credential,
|
||||
)
|
||||
@@ -104,7 +121,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
)
|
||||
|
||||
def get_secret(self, secret_key):
|
||||
credential = self._get_client_secret_credential_obj({})
|
||||
credential = self._get_client_secret_credential_obj()
|
||||
secret_client = self.sdk.secrets.SecretClient(
|
||||
vault_url=self.vault_url, credential=credential,
|
||||
)
|
||||
@@ -180,7 +197,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"secret_key": creds.root_sp_key,
|
||||
"tenant_id": creds.root_tenant_id,
|
||||
},
|
||||
resource=AZURE_MANAGEMENT_API,
|
||||
resource=self.sdk.cloud.endpoints.resource_manager,
|
||||
)
|
||||
|
||||
response = self._create_management_group(
|
||||
@@ -305,7 +322,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
)
|
||||
|
||||
def create_tenant(self, payload: TenantCSPPayload):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException("Could not resolve token for tenant creation")
|
||||
|
||||
@@ -317,26 +334,33 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
}
|
||||
|
||||
result = self.sdk.requests.post(
|
||||
"https://management.azure.com/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview",
|
||||
f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.SignUp/createTenant?api-version=2020-01-01-preview",
|
||||
json=create_tenant_body,
|
||||
headers=create_tenant_headers,
|
||||
)
|
||||
|
||||
if result.status_code == 200:
|
||||
return self._ok(
|
||||
TenantCSPResult(
|
||||
**result.json(),
|
||||
tenant_admin_password=payload.password,
|
||||
tenant_admin_username=payload.user_id,
|
||||
)
|
||||
result_dict = result.json()
|
||||
tenant_id = result_dict.get("tenantId")
|
||||
tenant_admin_username = (
|
||||
f"{payload.user_id}@{payload.domain_name}.onmicrosoft.com"
|
||||
)
|
||||
self.update_tenant_creds(
|
||||
tenant_id,
|
||||
KeyVaultCredentials(
|
||||
tenant_id=tenant_id,
|
||||
tenant_admin_username=tenant_admin_username,
|
||||
tenant_admin_password=payload.password,
|
||||
),
|
||||
)
|
||||
return self._ok(TenantCSPResult(**result_dict))
|
||||
else:
|
||||
return self._error(result.json())
|
||||
|
||||
def create_billing_profile_creation(
|
||||
self, payload: BillingProfileCreationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for billing profile creation"
|
||||
@@ -348,7 +372,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"Authorization": f"Bearer {sp_token}",
|
||||
}
|
||||
|
||||
billing_account_create_url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles?api-version=2019-10-01-preview"
|
||||
billing_account_create_url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles?api-version=2019-10-01-preview"
|
||||
|
||||
result = self.sdk.requests.post(
|
||||
billing_account_create_url,
|
||||
@@ -368,7 +392,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_billing_profile_verification(
|
||||
self, payload: BillingProfileVerificationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for billing profile validation"
|
||||
@@ -393,7 +417,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_billing_profile_tenant_access(
|
||||
self, payload: BillingProfileTenantAccessCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
request_body = {
|
||||
"properties": {
|
||||
"principalTenantId": payload.tenant_id, # from tenant creation
|
||||
@@ -406,7 +430,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"Authorization": f"Bearer {sp_token}",
|
||||
}
|
||||
|
||||
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/createBillingRoleAssignment?api-version=2019-10-01-preview"
|
||||
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/createBillingRoleAssignment?api-version=2019-10-01-preview"
|
||||
|
||||
result = self.sdk.requests.post(url, headers=headers, json=request_body)
|
||||
if result.status_code == 201:
|
||||
@@ -417,12 +441,12 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_task_order_billing_creation(
|
||||
self, payload: TaskOrderBillingCreationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
request_body = [
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/enabledAzurePlans",
|
||||
"value": [{"skuId": "0001"}],
|
||||
"value": [{"skuId": AZURE_SKU_ID}],
|
||||
}
|
||||
]
|
||||
|
||||
@@ -430,7 +454,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"Authorization": f"Bearer {sp_token}",
|
||||
}
|
||||
|
||||
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}?api-version=2019-10-01-preview"
|
||||
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}?api-version=2019-10-01-preview"
|
||||
|
||||
result = self.sdk.requests.patch(
|
||||
url, headers=request_headers, json=request_body
|
||||
@@ -447,7 +471,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_task_order_billing_verification(
|
||||
self, payload: TaskOrderBillingVerificationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for task order billing validation"
|
||||
@@ -470,7 +494,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
return self._error(result.json())
|
||||
|
||||
def create_billing_instruction(self, payload: BillingInstructionCSPPayload):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for task order billing validation"
|
||||
@@ -484,7 +508,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
}
|
||||
}
|
||||
|
||||
url = f"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/instructions/{payload.initial_task_order_id}:CLIN00{payload.initial_clin_type}?api-version=2019-10-01-preview"
|
||||
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Billing/billingAccounts/{payload.billing_account_name}/billingProfiles/{payload.billing_profile_name}/instructions/{payload.initial_task_order_id}:CLIN00{payload.initial_clin_type}?api-version=2019-10-01-preview"
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {sp_token}",
|
||||
@@ -498,7 +522,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
return self._error(result.json())
|
||||
|
||||
def create_product_purchase(self, payload: ProductPurchaseCSPPayload):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for aad premium product purchase"
|
||||
@@ -540,7 +564,7 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
def create_product_purchase_verification(
|
||||
self, payload: ProductPurchaseVerificationCSPPayload
|
||||
):
|
||||
sp_token = self._get_sp_token(payload.creds)
|
||||
sp_token = self._get_root_provisioning_token()
|
||||
if sp_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve token for aad premium product purchase validation"
|
||||
@@ -567,21 +591,198 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
else:
|
||||
return self._error(result.json())
|
||||
|
||||
def create_remote_admin(self, creds, tenant_details):
|
||||
# create app/service principal within tenant, with name constructed from tenant details
|
||||
# assign principal global admin
|
||||
def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload):
|
||||
mgmt_token = self._get_elevated_management_token(payload.tenant_id)
|
||||
|
||||
# needs to call out to CLI with tenant owner username/password, prototyping for that underway
|
||||
role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}"
|
||||
|
||||
# return identifier and creds to consumer for storage
|
||||
response = {"clientId": "string", "secretKey": "string", "tenantId": "string"}
|
||||
return self._ok(
|
||||
{
|
||||
"client_id": response["clientId"],
|
||||
"secret_key": response["secret_key"],
|
||||
"tenant_id": response["tenantId"],
|
||||
request_body = {
|
||||
"properties": {
|
||||
"roleDefinitionId": role_definition_id,
|
||||
"principalId": payload.user_object_id,
|
||||
}
|
||||
}
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {mgmt_token}",
|
||||
}
|
||||
|
||||
assignment_guid = str(uuid4())
|
||||
|
||||
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01"
|
||||
|
||||
response = self.sdk.requests.put(url, headers=auth_header, json=request_body)
|
||||
|
||||
if response.ok:
|
||||
return TenantAdminOwnershipCSPResult(**response.json())
|
||||
|
||||
def create_tenant_principal_ownership(
|
||||
self, payload: TenantPrincipalOwnershipCSPPayload
|
||||
):
|
||||
mgmt_token = self._get_elevated_management_token(payload.tenant_id)
|
||||
|
||||
# NOTE: the tenant_id is also the id of the root management group, once it is created
|
||||
role_definition_id = f"/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleDefinitions/{self.owner_role_def_id}"
|
||||
|
||||
request_body = {
|
||||
"properties": {
|
||||
"roleDefinitionId": role_definition_id,
|
||||
"principalId": payload.principal_id,
|
||||
}
|
||||
}
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {mgmt_token}",
|
||||
}
|
||||
|
||||
assignment_guid = str(uuid4())
|
||||
|
||||
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Management/managementGroups/{payload.tenant_id}/providers/Microsoft.Authorization/roleAssignments/{assignment_guid}?api-version=2015-07-01"
|
||||
|
||||
response = self.sdk.requests.put(url, headers=auth_header, json=request_body)
|
||||
|
||||
if response.ok:
|
||||
return TenantPrincipalOwnershipCSPResult(**response.json())
|
||||
|
||||
def create_tenant_principal_app(self, payload: TenantPrincipalAppCSPPayload):
|
||||
graph_token = self._get_tenant_admin_token(
|
||||
payload.tenant_id, self.graph_resource
|
||||
)
|
||||
if graph_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve graph token for tenant admin"
|
||||
)
|
||||
|
||||
request_body = {"displayName": "ATAT Remote Admin"}
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {graph_token}",
|
||||
}
|
||||
|
||||
url = f"{self.graph_resource}/v1.0/applications"
|
||||
|
||||
response = self.sdk.requests.post(url, json=request_body, headers=auth_header)
|
||||
|
||||
if response.ok:
|
||||
return TenantPrincipalAppCSPResult(**response.json())
|
||||
|
||||
def create_tenant_principal(self, payload: TenantPrincipalCSPPayload):
|
||||
graph_token = self._get_tenant_admin_token(
|
||||
payload.tenant_id, self.graph_resource
|
||||
)
|
||||
if graph_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve graph token for tenant admin"
|
||||
)
|
||||
|
||||
request_body = {"appId": payload.principal_app_id}
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {graph_token}",
|
||||
}
|
||||
|
||||
url = f"{self.graph_resource}/beta/servicePrincipals"
|
||||
|
||||
response = self.sdk.requests.post(url, json=request_body, headers=auth_header)
|
||||
|
||||
if response.ok:
|
||||
return TenantPrincipalCSPResult(**response.json())
|
||||
|
||||
def create_tenant_principal_credential(
|
||||
self, payload: TenantPrincipalCredentialCSPPayload
|
||||
):
|
||||
graph_token = self._get_tenant_admin_token(
|
||||
payload.tenant_id, self.graph_resource
|
||||
)
|
||||
if graph_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve graph token for tenant admin"
|
||||
)
|
||||
|
||||
request_body = {
|
||||
"passwordCredentials": [{"displayName": "ATAT Generated Password"}]
|
||||
}
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {graph_token}",
|
||||
}
|
||||
|
||||
url = f"{self.graph_resource}/v1.0/applications/{payload.principal_app_object_id}/addPassword"
|
||||
|
||||
response = self.sdk.requests.post(url, json=request_body, headers=auth_header)
|
||||
|
||||
if response.ok:
|
||||
result = response.json()
|
||||
self.update_tenant_creds(
|
||||
payload.tenant_id,
|
||||
KeyVaultCredentials(
|
||||
tenant_id=payload.tenant_id,
|
||||
tenant_sp_key=result.get("secretText"),
|
||||
tenant_sp_client_id=payload.principal_app_id,
|
||||
),
|
||||
)
|
||||
return TenantPrincipalCredentialCSPResult(
|
||||
principal_client_id=payload.principal_app_id,
|
||||
principal_creds_established=True,
|
||||
)
|
||||
|
||||
def create_admin_role_definition(self, payload: AdminRoleDefinitionCSPPayload):
|
||||
graph_token = self._get_tenant_admin_token(
|
||||
payload.tenant_id, self.graph_resource
|
||||
)
|
||||
if graph_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve graph token for tenant admin"
|
||||
)
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {graph_token}",
|
||||
}
|
||||
|
||||
url = f"{self.graph_resource}/beta/roleManagement/directory/roleDefinitions"
|
||||
|
||||
response = self.sdk.requests.get(url, headers=auth_header)
|
||||
|
||||
result = response.json()
|
||||
roleList = result.get("value")
|
||||
|
||||
DEFAULT_ADMIN_RD_ID = "794bb258-3e31-42ff-9ee4-731a72f62851"
|
||||
admin_role_def_id = next(
|
||||
(
|
||||
role.get("id")
|
||||
for role in roleList
|
||||
if role.get("displayName") == "Company Administrator"
|
||||
),
|
||||
DEFAULT_ADMIN_RD_ID,
|
||||
)
|
||||
|
||||
return AdminRoleDefinitionCSPResult(admin_role_def_id=admin_role_def_id)
|
||||
|
||||
def create_principal_admin_role(self, payload: PrincipalAdminRoleCSPPayload):
|
||||
graph_token = self._get_tenant_admin_token(
|
||||
payload.tenant_id, self.graph_resource
|
||||
)
|
||||
if graph_token is None:
|
||||
raise AuthenticationException(
|
||||
"Could not resolve graph token for tenant admin"
|
||||
)
|
||||
|
||||
request_body = {
|
||||
"principalId": payload.principal_id,
|
||||
"roleDefinitionId": payload.admin_role_def_id,
|
||||
"resourceScope": "/",
|
||||
}
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {graph_token}",
|
||||
}
|
||||
|
||||
url = f"{self.graph_resource}/beta/roleManagement/directory/roleAssignments"
|
||||
|
||||
response = self.sdk.requests.post(url, headers=auth_header, json=request_body)
|
||||
|
||||
if response.ok:
|
||||
return PrincipalAdminRoleCSPResult(**response.json())
|
||||
|
||||
def force_tenant_admin_pw_update(self, creds, tenant_owner_id):
|
||||
# use creds to update to force password recovery?
|
||||
@@ -651,22 +852,42 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
if sub_id_match:
|
||||
return sub_id_match.group(1)
|
||||
|
||||
def _get_sp_token(self, creds):
|
||||
home_tenant_id = creds.get("home_tenant_id")
|
||||
client_id = creds.get("client_id")
|
||||
secret_key = creds.get("secret_key")
|
||||
def _get_tenant_admin_token(self, tenant_id, resource):
|
||||
creds = self._source_tenant_creds(tenant_id)
|
||||
return self._get_up_token_for_resource(
|
||||
creds.tenant_admin_username,
|
||||
creds.tenant_admin_password,
|
||||
tenant_id,
|
||||
resource,
|
||||
)
|
||||
|
||||
# TODO: Make endpoints consts or configs
|
||||
authentication_endpoint = "https://login.microsoftonline.com/"
|
||||
resource = "https://management.azure.com/"
|
||||
def _get_root_provisioning_token(self):
|
||||
creds = self._source_creds()
|
||||
return self._get_sp_token(
|
||||
creds.tenant_id, creds.root_sp_client_id, creds.root_sp_key
|
||||
)
|
||||
|
||||
def _get_sp_token(self, tenant_id, client_id, secret_key):
|
||||
context = self.sdk.adal.AuthenticationContext(
|
||||
authentication_endpoint + home_tenant_id
|
||||
f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
|
||||
)
|
||||
|
||||
# TODO: handle failure states here
|
||||
token_response = context.acquire_token_with_client_credentials(
|
||||
resource, client_id, secret_key
|
||||
self.sdk.cloud.endpoints.resource_manager, client_id, secret_key
|
||||
)
|
||||
|
||||
return token_response.get("accessToken", None)
|
||||
|
||||
def _get_up_token_for_resource(self, username, password, tenant_id, resource):
|
||||
|
||||
context = self.sdk.adal.AuthenticationContext(
|
||||
f"{self.sdk.cloud.endpoints.active_directory}/{tenant_id}"
|
||||
)
|
||||
|
||||
# TODO: handle failure states here
|
||||
token_response = context.acquire_token_with_username_password(
|
||||
resource, username, password, self.ps_client_id
|
||||
)
|
||||
|
||||
return token_response.get("accessToken", None)
|
||||
@@ -680,16 +901,14 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
cloud_environment=self.sdk.cloud,
|
||||
)
|
||||
|
||||
def _get_client_secret_credential_obj(self, creds):
|
||||
def _get_client_secret_credential_obj(self):
|
||||
creds = self._source_creds()
|
||||
return self.sdk.identity.ClientSecretCredential(
|
||||
tenant_id=creds.get("tenant_id"),
|
||||
client_id=creds.get("client_id"),
|
||||
client_secret=creds.get("secret_key"),
|
||||
tenant_id=creds.tenant_id,
|
||||
client_id=creds.root_sp_client_id,
|
||||
client_secret=creds.root_sp_key,
|
||||
)
|
||||
|
||||
def _make_tenant_admin_cred_obj(self, username, password):
|
||||
return self.sdk.credentials.UserPassCredentials(username, password)
|
||||
|
||||
def _ok(self, body=None):
|
||||
return self._make_response("ok", body)
|
||||
|
||||
@@ -716,6 +935,26 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
"tenant_id": self.tenant_id,
|
||||
}
|
||||
|
||||
def _get_elevated_management_token(self, tenant_id):
|
||||
mgmt_token = self._get_tenant_admin_token(
|
||||
tenant_id, self.sdk.cloud.endpoints.resource_manager
|
||||
)
|
||||
if mgmt_token is None:
|
||||
raise AuthenticationException(
|
||||
"Failed to resolve management token for tenant admin"
|
||||
)
|
||||
|
||||
auth_header = {
|
||||
"Authorization": f"Bearer {mgmt_token}",
|
||||
}
|
||||
url = f"{self.sdk.cloud.endpoints.resource_manager}/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"
|
||||
result = self.sdk.requests.post(url, headers=auth_header)
|
||||
|
||||
if not result.ok:
|
||||
raise AuthenticationException("Failed to elevate access")
|
||||
|
||||
return mgmt_token
|
||||
|
||||
def _source_creds(self, tenant_id=None) -> KeyVaultCredentials:
|
||||
if tenant_id:
|
||||
return self._source_tenant_creds(tenant_id)
|
||||
@@ -726,13 +965,16 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
root_sp_key=self._root_creds.get("secret_key"),
|
||||
)
|
||||
|
||||
def update_tenant_creds(self, tenant_id, secret):
|
||||
def update_tenant_creds(self, tenant_id, secret: KeyVaultCredentials):
|
||||
hashed = sha256_hex(tenant_id)
|
||||
self.set_secret(hashed, json.dumps(secret))
|
||||
new_secrets = secret.dict()
|
||||
curr_secrets = self._source_tenant_creds(tenant_id)
|
||||
updated_secrets: Dict[str, Any] = {**curr_secrets.dict(), **new_secrets}
|
||||
us = KeyVaultCredentials(**updated_secrets)
|
||||
self.set_secret(hashed, json.dumps(us.dict()))
|
||||
return us
|
||||
|
||||
return secret
|
||||
|
||||
def _source_tenant_creds(self, tenant_id):
|
||||
def _source_tenant_creds(self, tenant_id) -> KeyVaultCredentials:
|
||||
hashed = sha256_hex(tenant_id)
|
||||
raw_creds = self.get_secret(hashed)
|
||||
return KeyVaultCredentials(**json.loads(raw_creds))
|
||||
|
@@ -1,41 +1,52 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from atst.domain.csp.cloud.exceptions import (
|
||||
BaselineProvisionException,
|
||||
EnvironmentCreationException,
|
||||
GeneralCSPException,
|
||||
UserProvisioningException,
|
||||
UserRemovalException,
|
||||
)
|
||||
from atst.domain.csp.cloud.models import BillingProfileTenantAccessCSPResult
|
||||
|
||||
from .cloud_provider_interface import CloudProviderInterface
|
||||
from .exceptions import (
|
||||
AuthenticationException,
|
||||
AuthorizationException,
|
||||
BaselineProvisionException,
|
||||
ConnectionException,
|
||||
EnvironmentCreationException,
|
||||
GeneralCSPException,
|
||||
UnknownServerException,
|
||||
UserProvisioningException,
|
||||
UserRemovalException,
|
||||
)
|
||||
from .models import (
|
||||
AZURE_MGMNT_PATH,
|
||||
AdminRoleDefinitionCSPPayload,
|
||||
AdminRoleDefinitionCSPResult,
|
||||
ApplicationCSPPayload,
|
||||
ApplicationCSPResult,
|
||||
BillingInstructionCSPPayload,
|
||||
BillingInstructionCSPResult,
|
||||
BillingProfileCreationCSPPayload,
|
||||
BillingProfileCreationCSPResult,
|
||||
BillingProfileTenantAccessCSPResult,
|
||||
BillingProfileVerificationCSPPayload,
|
||||
BillingProfileVerificationCSPResult,
|
||||
ProductPurchaseCSPPayload,
|
||||
ProductPurchaseCSPResult,
|
||||
ProductPurchaseVerificationCSPPayload,
|
||||
ProductPurchaseVerificationCSPResult,
|
||||
PrincipalAdminRoleCSPPayload,
|
||||
PrincipalAdminRoleCSPResult,
|
||||
TaskOrderBillingCreationCSPPayload,
|
||||
TaskOrderBillingCreationCSPResult,
|
||||
TaskOrderBillingVerificationCSPPayload,
|
||||
TaskOrderBillingVerificationCSPResult,
|
||||
TenantAdminOwnershipCSPPayload,
|
||||
TenantAdminOwnershipCSPResult,
|
||||
TenantCSPPayload,
|
||||
TenantCSPResult,
|
||||
TenantPrincipalAppCSPPayload,
|
||||
TenantPrincipalAppCSPResult,
|
||||
TenantPrincipalCredentialCSPPayload,
|
||||
TenantPrincipalCredentialCSPResult,
|
||||
TenantPrincipalCSPPayload,
|
||||
TenantPrincipalCSPResult,
|
||||
TenantPrincipalOwnershipCSPPayload,
|
||||
TenantPrincipalOwnershipCSPResult,
|
||||
)
|
||||
|
||||
|
||||
@@ -124,7 +135,7 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
payload is an instance of TenantCSPPayload data class
|
||||
"""
|
||||
|
||||
self._authorize(payload.creds)
|
||||
self._authorize("admin")
|
||||
|
||||
self._delay(1, 5)
|
||||
|
||||
@@ -304,6 +315,70 @@ class MockCloudProvider(CloudProviderInterface):
|
||||
**dict(premium_purchase_date="2020-01-30T18:57:05.981Z")
|
||||
)
|
||||
|
||||
def create_tenant_admin_ownership(self, payload: TenantAdminOwnershipCSPPayload):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
return TenantAdminOwnershipCSPResult(**dict(id="admin_owner_assignment_id"))
|
||||
|
||||
|
||||
def create_tenant_principal_ownership(
|
||||
self, payload: TenantPrincipalOwnershipCSPPayload
|
||||
):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
|
||||
return TenantPrincipalOwnershipCSPResult(
|
||||
**dict(id="principal_owner_assignment_id")
|
||||
)
|
||||
|
||||
def create_tenant_principal_app(self, payload: TenantPrincipalAppCSPPayload):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
|
||||
return TenantPrincipalAppCSPResult(
|
||||
**dict(appId="principal_app_id", id="principal_app_object_id")
|
||||
)
|
||||
|
||||
def create_tenant_principal(self, payload: TenantPrincipalCSPPayload):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
|
||||
return TenantPrincipalCSPResult(**dict(id="principal_id"))
|
||||
|
||||
def create_tenant_principal_credential(
|
||||
self, payload: TenantPrincipalCredentialCSPPayload
|
||||
):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
|
||||
return TenantPrincipalCredentialCSPResult(
|
||||
**dict(
|
||||
principal_client_id="principal_client_id",
|
||||
principal_creds_established=True,
|
||||
)
|
||||
)
|
||||
|
||||
def create_admin_role_definition(self, payload: AdminRoleDefinitionCSPPayload):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
|
||||
return AdminRoleDefinitionCSPResult(
|
||||
**dict(admin_role_def_id="admin_role_def_id")
|
||||
)
|
||||
|
||||
def create_principal_admin_role(self, payload: PrincipalAdminRoleCSPPayload):
|
||||
self._maybe_raise(self.NETWORK_FAILURE_PCT, self.NETWORK_EXCEPTION)
|
||||
self._maybe_raise(self.SERVER_FAILURE_PCT, self.SERVER_EXCEPTION)
|
||||
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
|
||||
|
||||
return PrincipalAdminRoleCSPResult(**dict(id="principal_assignment_id"))
|
||||
|
||||
def create_or_update_user(self, auth_credentials, user_info, csp_role_id):
|
||||
self._authorize(auth_credentials)
|
||||
|
||||
|
@@ -22,20 +22,10 @@ class AliasModel(BaseModel):
|
||||
|
||||
|
||||
class BaseCSPPayload(AliasModel):
|
||||
# {"username": "mock-cloud", "pass": "shh"}
|
||||
creds: Dict
|
||||
|
||||
def dict(self, *args, **kwargs):
|
||||
exclude = {"creds"}
|
||||
if "exclude" not in kwargs:
|
||||
kwargs["exclude"] = exclude
|
||||
else:
|
||||
kwargs["exclude"].update(exclude)
|
||||
|
||||
return super().dict(*args, **kwargs)
|
||||
tenant_id: str
|
||||
|
||||
|
||||
class TenantCSPPayload(BaseCSPPayload):
|
||||
class TenantCSPPayload(AliasModel):
|
||||
user_id: str
|
||||
password: Optional[str]
|
||||
domain_name: str
|
||||
@@ -236,6 +226,81 @@ class BillingInstructionCSPResult(AliasModel):
|
||||
}
|
||||
|
||||
|
||||
class TenantAdminOwnershipCSPPayload(BaseCSPPayload):
|
||||
user_object_id: str
|
||||
|
||||
|
||||
class TenantAdminOwnershipCSPResult(AliasModel):
|
||||
admin_owner_assignment_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"admin_owner_assignment_id": "id"}
|
||||
|
||||
|
||||
class TenantPrincipalOwnershipCSPPayload(BaseCSPPayload):
|
||||
principal_id: str
|
||||
|
||||
|
||||
class TenantPrincipalOwnershipCSPResult(AliasModel):
|
||||
principal_owner_assignment_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"principal_owner_assignment_id": "id"}
|
||||
|
||||
|
||||
class TenantPrincipalAppCSPPayload(BaseCSPPayload):
|
||||
pass
|
||||
|
||||
|
||||
class TenantPrincipalAppCSPResult(AliasModel):
|
||||
principal_app_id: str
|
||||
principal_app_object_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"principal_app_id": "appId", "principal_app_object_id": "id"}
|
||||
|
||||
|
||||
class TenantPrincipalCSPPayload(BaseCSPPayload):
|
||||
principal_app_id: str
|
||||
|
||||
|
||||
class TenantPrincipalCSPResult(AliasModel):
|
||||
principal_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"principal_id": "id"}
|
||||
|
||||
|
||||
class TenantPrincipalCredentialCSPPayload(BaseCSPPayload):
|
||||
principal_app_id: str
|
||||
principal_app_object_id: str
|
||||
|
||||
|
||||
class TenantPrincipalCredentialCSPResult(AliasModel):
|
||||
principal_client_id: str
|
||||
principal_creds_established: bool
|
||||
|
||||
|
||||
class AdminRoleDefinitionCSPPayload(BaseCSPPayload):
|
||||
pass
|
||||
|
||||
|
||||
class AdminRoleDefinitionCSPResult(AliasModel):
|
||||
admin_role_def_id: str
|
||||
|
||||
|
||||
class PrincipalAdminRoleCSPPayload(BaseCSPPayload):
|
||||
principal_id: str
|
||||
admin_role_def_id: str
|
||||
|
||||
|
||||
class PrincipalAdminRoleCSPResult(AliasModel):
|
||||
principal_assignment_id: str
|
||||
|
||||
class Config:
|
||||
fields = {"principal_assignment_id": "id"}
|
||||
|
||||
|
||||
AZURE_MGMNT_PATH = "/providers/Microsoft.Management/managementGroups/"
|
||||
|
||||
MANAGEMENT_GROUP_NAME_REGEX = "^[a-zA-Z0-9\-_\(\)\.]+$"
|
||||
|
@@ -19,6 +19,13 @@ class AzureStages(Enum):
|
||||
BILLING_INSTRUCTION = "billing instruction"
|
||||
PRODUCT_PURCHASE = "purchase aad premium product"
|
||||
PRODUCT_PURCHASE_VERIFICATION = "purchase aad premium product verification"
|
||||
TENANT_PRINCIPAL_APP = "tenant principal application"
|
||||
TENANT_PRINCIPAL = "tenant principal"
|
||||
TENANT_PRINCIPAL_CREDENTIAL = "tenant principal credential"
|
||||
ADMIN_ROLE_DEFINITION = "admin role definition"
|
||||
PRINCIPAL_ADMIN_ROLE = "tenant principal admin"
|
||||
TENANT_ADMIN_OWNERSHIP = "tenant admin ownership"
|
||||
TENANT_PRINCIPAL_OWNERSHIP = "tenant principial ownership"
|
||||
|
||||
|
||||
def _build_csp_states(csp_stages):
|
||||
|
@@ -168,14 +168,6 @@ class PortfolioStateMachine(
|
||||
self.portfolio.csp_data.update(response.dict())
|
||||
db.session.add(self.portfolio)
|
||||
db.session.commit()
|
||||
|
||||
if getattr(response, "get_creds", None) is not None:
|
||||
new_creds = response.get_creds()
|
||||
# TODO: one way salted hash of tenant_id to use as kv key name?
|
||||
tenant_id = new_creds.get("tenant_id")
|
||||
secret = self.csp.get_secret(tenant_id, new_creds)
|
||||
secret.update(new_creds)
|
||||
self.csp.update_tenant_creds(tenant_id, secret)
|
||||
except PydanticValidationError as exc:
|
||||
app.logger.error(
|
||||
f"Failed to cast response to valid result class {self.__repr__()}:",
|
||||
|
@@ -14,7 +14,9 @@ from flask import (
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
import pendulum
|
||||
import os
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.exceptions import NotFound, MethodNotAllowed
|
||||
from werkzeug.routing import RequestRedirect
|
||||
|
||||
|
||||
from atst.domain.users import Users
|
||||
from atst.domain.authnid import AuthenticationContext
|
||||
@@ -61,17 +63,36 @@ def _make_authentication_context():
|
||||
|
||||
|
||||
def redirect_after_login_url():
|
||||
if request.args.get("next"):
|
||||
returl = request.args.get("next")
|
||||
if request.args.get(app.form_cache.PARAM_NAME):
|
||||
returl += "?" + url.urlencode(
|
||||
{app.form_cache.PARAM_NAME: request.args.get(app.form_cache.PARAM_NAME)}
|
||||
)
|
||||
returl = request.args.get("next")
|
||||
if match_url_pattern(returl):
|
||||
param_name = request.args.get(app.form_cache.PARAM_NAME)
|
||||
if param_name:
|
||||
returl += "?" + url.urlencode({app.form_cache.PARAM_NAME: param_name})
|
||||
return returl
|
||||
else:
|
||||
return url_for("atst.home")
|
||||
|
||||
|
||||
def match_url_pattern(url, method="GET"):
|
||||
"""Ensure a url matches a url pattern in the flask app
|
||||
inspired by https://stackoverflow.com/questions/38488134/get-the-flask-view-function-that-matches-a-url/38488506#38488506
|
||||
"""
|
||||
server_name = app.config.get("SERVER_NAME") or "localhost"
|
||||
adapter = app.url_map.bind(server_name=server_name)
|
||||
|
||||
try:
|
||||
match = adapter.match(url, method=method)
|
||||
except RequestRedirect as e:
|
||||
# recursively match redirects
|
||||
return match_url_pattern(e.new_url, method)
|
||||
except (MethodNotAllowed, NotFound):
|
||||
# no match
|
||||
return None
|
||||
|
||||
if match[0] in app.view_functions:
|
||||
return url
|
||||
|
||||
|
||||
def current_user_setup(user):
|
||||
session["user_id"] = user.id
|
||||
session["last_login"] = user.last_login
|
||||
@@ -109,13 +130,3 @@ def logout():
|
||||
@bp.route("/about")
|
||||
def about():
|
||||
return render_template("about.html")
|
||||
|
||||
|
||||
@bp.route("/csp-environment-access")
|
||||
def csp_environment_access():
|
||||
return render_template("mock_csp.html", text="console for this environment")
|
||||
|
||||
|
||||
@bp.route("/jedi-csp-calculator")
|
||||
def jedi_csp_calculator():
|
||||
return redirect(app.csp.cloud.get_calculator_url())
|
||||
|
@@ -22,9 +22,4 @@ def wrap_environment_role_lookup(user, environment_id=None, **kwargs):
|
||||
@applications_bp.route("/environments/<environment_id>/access")
|
||||
@user_can(None, override=wrap_environment_role_lookup, message="access environment")
|
||||
def access_environment(environment_id):
|
||||
env_role = EnvironmentRoles.get_by_user_and_environment(
|
||||
g.current_user.id, environment_id
|
||||
)
|
||||
login_url = app.csp.cloud.get_environment_login_url(env_role.environment)
|
||||
|
||||
return redirect(url_for("atst.csp_environment_access", login_url=login_url))
|
||||
return redirect("https://portal.azure.com")
|
||||
|
@@ -70,7 +70,12 @@ def update_task_order(form, portfolio_id=None, task_order_id=None, flash_invalid
|
||||
|
||||
|
||||
def update_and_render_next(
|
||||
form_data, next_page, current_template, portfolio_id=None, task_order_id=None
|
||||
form_data,
|
||||
next_page,
|
||||
current_template,
|
||||
portfolio_id=None,
|
||||
task_order_id=None,
|
||||
previous=False,
|
||||
):
|
||||
form = None
|
||||
if task_order_id:
|
||||
@@ -80,8 +85,9 @@ def update_and_render_next(
|
||||
form = TaskOrderForm(form_data)
|
||||
|
||||
task_order = update_task_order(form, portfolio_id, task_order_id)
|
||||
if task_order:
|
||||
return redirect(url_for(next_page, task_order_id=task_order.id))
|
||||
if task_order or previous:
|
||||
to_id = task_order.id if task_order else task_order_id
|
||||
return redirect(url_for(next_page, task_order_id=to_id))
|
||||
else:
|
||||
return (
|
||||
render_task_orders_edit(
|
||||
@@ -210,12 +216,21 @@ def form_step_two_add_number(task_order_id):
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_2", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
|
||||
def submit_form_step_two_add_number(task_order_id):
|
||||
previous = http_request.args.get("previous", "False").lower() == "true"
|
||||
form_data = {**http_request.form}
|
||||
next_page = "task_orders.form_step_three_add_clins"
|
||||
next_page = (
|
||||
"task_orders.form_step_three_add_clins"
|
||||
if not previous
|
||||
else "task_orders.form_step_one_add_pdf"
|
||||
)
|
||||
current_template = "task_orders/step_2.html"
|
||||
|
||||
return update_and_render_next(
|
||||
form_data, next_page, current_template, task_order_id=task_order_id
|
||||
form_data,
|
||||
next_page,
|
||||
current_template,
|
||||
task_order_id=task_order_id,
|
||||
previous=previous,
|
||||
)
|
||||
|
||||
|
||||
@@ -230,12 +245,21 @@ def form_step_three_add_clins(task_order_id):
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/form/step_3", methods=["POST"])
|
||||
@user_can(Permissions.CREATE_TASK_ORDER, message="update task order form")
|
||||
def submit_form_step_three_add_clins(task_order_id):
|
||||
previous = http_request.args.get("previous", "False").lower() == "true"
|
||||
form_data = {**http_request.form}
|
||||
next_page = "task_orders.form_step_four_review"
|
||||
next_page = (
|
||||
"task_orders.form_step_four_review"
|
||||
if not previous
|
||||
else "task_orders.form_step_two_add_number"
|
||||
)
|
||||
current_template = "task_orders/step_3.html"
|
||||
|
||||
return update_and_render_next(
|
||||
form_data, next_page, current_template, task_order_id=task_order_id
|
||||
form_data,
|
||||
next_page,
|
||||
current_template,
|
||||
task_order_id=task_order_id,
|
||||
previous=previous,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -3,6 +3,7 @@ from flask import Blueprint, render_template, g, request as http_request, redire
|
||||
from atst.forms.edit_user import EditUserForm
|
||||
from atst.domain.users import Users
|
||||
from atst.utils.flash import formatted_flash as flash
|
||||
from atst.routes import match_url_pattern
|
||||
|
||||
|
||||
bp = Blueprint("users", __name__)
|
||||
@@ -35,7 +36,7 @@ def update_user():
|
||||
if form.validate():
|
||||
Users.update(user, form.data)
|
||||
flash("user_updated")
|
||||
if next_url:
|
||||
if match_url_pattern(next_url):
|
||||
return redirect(next_url)
|
||||
|
||||
return render_template(
|
||||
|
Reference in New Issue
Block a user