Update the registration logic to print manual steps when adal authentication fails (#447)

Mitigate the deployment issue related to the conditional access policy.
The registration logic is updated to use the old rbac python library when possible. 
The deployment will print some manual step for operations that cannot be automated
This commit is contained in:
Cheick Keita
2021-01-22 14:21:43 -08:00
committed by GitHub
parent 2f3139cda1
commit ec982c68c5
3 changed files with 206 additions and 106 deletions

View File

@ -67,7 +67,6 @@ from registration import (
add_application_password, add_application_password,
assign_scaleset_role, assign_scaleset_role,
authorize_application, authorize_application,
get_application,
register_application, register_application,
update_pool_registration, update_pool_registration,
) )
@ -329,10 +328,11 @@ class Client:
(password_id, password) = self.create_password(app.object_id) (password_id, password) = self.create_password(app.object_id)
onefuzz_cli_app_uuid = uuid.UUID(ONEFUZZ_CLI_APP) cli_app = client.applications.list(filter="appId eq '%s'" % ONEFUZZ_CLI_APP)
cli_app = get_application(onefuzz_cli_app_uuid)
if cli_app is None: onefuzz_cli_app_uuid = uuid.UUID(ONEFUZZ_CLI_APP)
if not cli_app:
logger.info( logger.info(
"Could not find the default CLI application under the current " "Could not find the default CLI application under the current "
"subscription, creating a new one" "subscription, creating a new one"

View File

@ -12,6 +12,7 @@ from enum import Enum
from typing import Any, Dict, List, NamedTuple, Optional, Tuple from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from uuid import UUID, uuid4 from uuid import UUID, uuid4
import adal # type: ignore
import requests import requests
from azure.common.client_factory import get_client_from_cli_profile from azure.common.client_factory import get_client_from_cli_profile
from azure.common.credentials import get_cli_profile from azure.common.credentials import get_cli_profile
@ -19,12 +20,21 @@ from azure.graphrbac import GraphRbacManagementClient
from azure.graphrbac.models import ( from azure.graphrbac.models import (
Application, Application,
ApplicationCreateParameters, ApplicationCreateParameters,
ApplicationUpdateParameters,
AppRole,
PasswordCredential,
RequiredResourceAccess, RequiredResourceAccess,
ResourceAccess, ResourceAccess,
ServicePrincipal,
) )
from functional import seq from functional import seq
from msrest.serialization import TZ_UTC from msrest.serialization import TZ_UTC
FIX_URL = (
"https://ms.portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/"
"ApplicationMenuBlade/ProtectAnAPI/appId/%s/isMSAApp/"
)
logger = logging.getLogger("deploy") logger = logging.getLogger("deploy")
@ -70,6 +80,13 @@ def query_microsoft_graph(
) )
def get_graph_client() -> GraphRbacManagementClient:
client: GraphRbacManagementClient = get_client_from_cli_profile(
GraphRbacManagementClient
)
return client
class ApplicationInfo(NamedTuple): class ApplicationInfo(NamedTuple):
client_id: UUID client_id: UUID
client_secret: str client_secret: str
@ -85,7 +102,7 @@ def register_application(
registration_name: str, onefuzz_instance_name: str, approle: OnefuzzAppRole registration_name: str, onefuzz_instance_name: str, approle: OnefuzzAppRole
) -> ApplicationInfo: ) -> ApplicationInfo:
logger.info("retrieving the application registration %s" % registration_name) logger.info("retrieving the application registration %s" % registration_name)
client = get_client_from_cli_profile(GraphRbacManagementClient) client = get_graph_client()
apps: List[Application] = list( apps: List[Application] = list(
client.applications.list(filter="displayName eq '%s'" % registration_name) client.applications.list(filter="displayName eq '%s'" % registration_name)
) )
@ -132,7 +149,7 @@ def create_application_credential(application_name: str) -> str:
""" Add a new password to the application registration """ """ Add a new password to the application registration """
logger.info("creating application credential for '%s'" % application_name) logger.info("creating application credential for '%s'" % application_name)
client = get_client_from_cli_profile(GraphRbacManagementClient) client = get_graph_client()
apps: List[Application] = list( apps: List[Application] = list(
client.applications.list(filter="displayName eq '%s'" % application_name) client.applications.list(filter="displayName eq '%s'" % application_name)
) )
@ -148,7 +165,7 @@ def create_application_registration(
) -> Application: ) -> Application:
""" Create an application registration """ """ Create an application registration """
client = get_client_from_cli_profile(GraphRbacManagementClient) client = get_graph_client()
apps: List[Application] = list( apps: List[Application] = list(
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name) client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
) )
@ -189,22 +206,16 @@ def create_application_registration(
atttempts = atttempts - 1 atttempts = atttempts - 1
try: try:
time.sleep(5) time.sleep(5)
query_microsoft_graph(
method="PATCH", client = get_graph_client()
resource="applications/%s" % registered_app.object_id, update_param = ApplicationUpdateParameters(
body={ reply_urls=["https://%s.azurewebsites.net" % onefuzz_instance_name]
"publicClient": {
"redirectUris": [
"https://%s.azurewebsites.net" % onefuzz_instance_name
]
},
"isFallbackPublicClient": True,
},
) )
client.applications.patch(registered_app.object_id, update_param)
break break
except GraphQueryError as err: except Exception:
if err.status_code == 404: continue
continue
authorize_application(UUID(registered_app.app_id), UUID(app.app_id)) authorize_application(UUID(registered_app.app_id), UUID(app.app_id))
return registered_app return registered_app
@ -232,6 +243,26 @@ def add_application_password(app_object_id: UUID) -> Tuple[str, str]:
raise Exception("unable to create password") raise Exception("unable to create password")
def add_application_password_legacy(app_object_id: UUID) -> Tuple[str, str]:
key = str(uuid4())
password = str(uuid4())
client = get_graph_client()
password_cred = [
PasswordCredential(
start_date="%s" % datetime.now(TZ_UTC).strftime("%Y-%m-%dT%H:%M.%fZ"),
end_date="%s"
% (datetime.now(TZ_UTC) + timedelta(days=365)).strftime(
"%Y-%m-%dT%H:%M.%fZ"
),
key_id=key,
value=password,
)
]
client.applications.update_password_credentials(app_object_id, password_cred)
return (key, password)
def add_application_password_impl(app_object_id: UUID) -> Tuple[str, str]: def add_application_password_impl(app_object_id: UUID) -> Tuple[str, str]:
key = uuid4() key = uuid4()
password_request = { password_request = {
@ -245,13 +276,15 @@ def add_application_password_impl(app_object_id: UUID) -> Tuple[str, str]:
} }
} }
password: Dict = query_microsoft_graph( try:
method="POST", password: Dict = query_microsoft_graph(
resource="applications/%s/addPassword" % app_object_id, method="POST",
body=password_request, resource="applications/%s/addPassword" % app_object_id,
) body=password_request,
)
return (str(key), password["secretText"]) return (str(key), password["secretText"])
except adal.AdalError:
return add_application_password_legacy(app_object_id)
def get_application(app_id: UUID) -> Optional[Any]: def get_application(app_id: UUID) -> Optional[Any]:
@ -271,40 +304,46 @@ def authorize_application(
onefuzz_app_id: UUID, onefuzz_app_id: UUID,
permissions: List[str] = ["user_impersonation"], permissions: List[str] = ["user_impersonation"],
) -> None: ) -> None:
onefuzz_app = get_application(onefuzz_app_id) try:
if onefuzz_app is None: onefuzz_app = get_application(onefuzz_app_id)
logger.error("Application '%s' not found" % onefuzz_app_id) if onefuzz_app is None:
return logger.error("Application '%s' not found", onefuzz_app_id)
return
scopes = seq(onefuzz_app["api"]["oauth2PermissionScopes"]).filter( scopes = seq(onefuzz_app["api"]["oauth2PermissionScopes"]).filter(
lambda scope: scope["value"] in permissions lambda scope: scope["value"] in permissions
)
existing_preAuthorizedApplications = (
seq(onefuzz_app["api"]["preAuthorizedApplications"])
.map(
lambda paa: seq(paa["delegatedPermissionIds"]).map(
lambda permission_id: (paa["appId"], permission_id)
)
) )
.flatten()
)
preAuthorizedApplications = ( existing_preAuthorizedApplications = (
scopes.map(lambda s: (str(registration_app_id), s["id"])) seq(onefuzz_app["api"]["preAuthorizedApplications"])
.union(existing_preAuthorizedApplications) .map(
.distinct() lambda paa: seq(paa["delegatedPermissionIds"]).map(
.group_by_key() lambda permission_id: (paa["appId"], permission_id)
.map(lambda data: {"appId": data[0], "delegatedPermissionIds": data[1]}) )
) )
.flatten()
)
query_microsoft_graph( preAuthorizedApplications = (
method="PATCH", scopes.map(lambda s: (str(registration_app_id), s["id"]))
resource="applications/%s" % onefuzz_app["id"], .union(existing_preAuthorizedApplications)
body={ .distinct()
"api": {"preAuthorizedApplications": preAuthorizedApplications.to_list()} .group_by_key()
}, .map(lambda data: {"appId": data[0], "delegatedPermissionIds": data[1]})
) )
query_microsoft_graph(
method="PATCH",
resource="applications/%s" % onefuzz_app["id"],
body={
"api": {
"preAuthorizedApplications": preAuthorizedApplications.to_list()
}
},
)
except adal.AdalError:
logger.warning("*** Browse to: %s", FIX_URL % onefuzz_app_id)
logger.warning("*** Then add the client application %s", registration_app_id)
def create_and_display_registration( def create_and_display_registration(
@ -330,78 +369,138 @@ def update_pool_registration(onefuzz_instance_name: str) -> None:
) )
def assign_scaleset_role(onefuzz_instance_name: str, scaleset_name: str) -> None: def assign_scaleset_role_manually(
""" onefuzz_instance_name: str, scaleset_name: str
Allows the nodes in the scaleset to access the service by assigning ) -> None:
their managed identity to the ManagedNode Role
"""
onefuzz_service_appId = query_microsoft_graph( client = get_graph_client()
method="GET", apps: List[Application] = list(
resource="applications", client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
params={
"$filter": "displayName eq '%s'" % onefuzz_instance_name,
"$select": "appId",
},
) )
if len(onefuzz_service_appId["value"]) == 0: if not apps:
raise Exception("onefuzz app registration not found") raise Exception("onefuzz app registration not found")
appId = onefuzz_service_appId["value"][0]["appId"]
onefuzz_service_principals = query_microsoft_graph( app = apps[0]
method="GET", appId = app.app_id
resource="servicePrincipals",
params={"$filter": "appId eq '%s'" % appId}, onefuzz_service_principals: List[ServicePrincipal] = list(
client.service_principals.list(filter="appId eq '%s'" % appId)
) )
if len(onefuzz_service_principals["value"]) == 0: if not onefuzz_service_principals:
raise Exception("onefuzz app service principal not found") raise Exception("onefuzz app service principal not found")
onefuzz_service_principal = onefuzz_service_principals["value"][0] onefuzz_service_principal = onefuzz_service_principals[0]
scaleset_service_principals = query_microsoft_graph( scaleset_service_principals: List[ServicePrincipal] = list(
method="GET", client.service_principals.list(filter="displayName eq '%s'" % scaleset_name)
resource="servicePrincipals",
params={"$filter": "displayName eq '%s'" % scaleset_name},
) )
if len(scaleset_service_principals["value"]) == 0:
if not scaleset_service_principals:
raise Exception("scaleset service principal not found") raise Exception("scaleset service principal not found")
scaleset_service_principal = scaleset_service_principals["value"][0] scaleset_service_principal = scaleset_service_principals[0]
managed_node_role = ( scaleset_service_principal.app_roles
seq(onefuzz_service_principal["appRoles"]) app_roles: List[AppRole] = [
.filter(lambda x: x["value"] == OnefuzzAppRole.ManagedNode.value) role for role in app.app_roles if role.value == OnefuzzAppRole.ManagedNode.value
.head_option() ]
)
if not managed_node_role: if not app_roles:
raise Exception( raise Exception(
"ManagedNode role not found in the OneFuzz application " "ManagedNode role not found in the OneFuzz application "
"registration. Please redeploy the instance" "registration. Please redeploy the instance"
) )
assignments = query_microsoft_graph( body = '{ "principalId": "%s", "resourceId": "%s", "appRoleId": "%s"}' % (
method="GET", scaleset_service_principal.object_id,
resource="servicePrincipals/%s/appRoleAssignments" onefuzz_service_principal.object_id,
% scaleset_service_principal["id"], app_roles[0].id,
) )
# check if the role is already assigned query = (
role_assigned = seq(assignments["value"]).find( "az rest --method post --url https://graph.microsoft.com/v1.0/servicePrincipals/%s/appRoleAssignedTo --body '%s' --headers \"Content-Type\"=application/json"
lambda assignment: assignment["appRoleId"] == managed_node_role["id"] % (scaleset_service_principal.object_id, body)
) )
if not role_assigned:
query_microsoft_graph( logger.warning(
method="POST", "execute the following query in the azure portal bash shell : \n%s" % query
resource="servicePrincipals/%s/appRoleAssignedTo" )
% scaleset_service_principal["id"],
body={
"principalId": scaleset_service_principal["id"], def assign_scaleset_role(onefuzz_instance_name: str, scaleset_name: str) -> None:
"resourceId": onefuzz_service_principal["id"], """
"appRoleId": managed_node_role["id"], Allows the nodes in the scaleset to access the service by assigning
their managed identity to the ManagedNode Role
"""
try:
onefuzz_service_appId = query_microsoft_graph(
method="GET",
resource="applications",
params={
"$filter": "displayName eq '%s'" % onefuzz_instance_name,
"$select": "appId",
}, },
) )
if len(onefuzz_service_appId["value"]) == 0:
raise Exception("onefuzz app registration not found")
appId = onefuzz_service_appId["value"][0]["appId"]
onefuzz_service_principals = query_microsoft_graph(
method="GET",
resource="servicePrincipals",
params={"$filter": "appId eq '%s'" % appId},
)
if len(onefuzz_service_principals["value"]) == 0:
raise Exception("onefuzz app service principal not found")
onefuzz_service_principal = onefuzz_service_principals["value"][0]
scaleset_service_principals = query_microsoft_graph(
method="GET",
resource="servicePrincipals",
params={"$filter": "displayName eq '%s'" % scaleset_name},
)
if len(scaleset_service_principals["value"]) == 0:
raise Exception("scaleset service principal not found")
scaleset_service_principal = scaleset_service_principals["value"][0]
managed_node_role = (
seq(onefuzz_service_principal["appRoles"])
.filter(lambda x: x["value"] == OnefuzzAppRole.ManagedNode.value)
.head_option()
)
if not managed_node_role:
raise Exception(
"ManagedNode role not found in the OneFuzz application "
"registration. Please redeploy the instance"
)
assignments = query_microsoft_graph(
method="GET",
resource="servicePrincipals/%s/appRoleAssignments"
% scaleset_service_principal["id"],
)
# check if the role is already assigned
role_assigned = seq(assignments["value"]).find(
lambda assignment: assignment["appRoleId"] == managed_node_role["id"]
)
if not role_assigned:
query_microsoft_graph(
method="POST",
resource="servicePrincipals/%s/appRoleAssignedTo"
% scaleset_service_principal["id"],
body={
"principalId": scaleset_service_principal["id"],
"resourceId": onefuzz_service_principal["id"],
"appRoleId": managed_node_role["id"],
},
)
except adal.AdalError:
assign_scaleset_role_manually(onefuzz_instance_name, scaleset_name)
def main() -> None: def main() -> None:
formatter = argparse.ArgumentDefaultsHelpFormatter formatter = argparse.ArgumentDefaultsHelpFormatter

View File

@ -12,3 +12,4 @@ azure-storage-queue==12.1.3
cryptography<3.0.0,>=2.3.1 cryptography<3.0.0,>=2.3.1
pyfunctional==1.4.2 pyfunctional==1.4.2
pyopenssl==19.1.0 pyopenssl==19.1.0
adal~=1.2.5