mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-15 03:18:07 +00:00
migrate to msgraph (#966)
* migrate to msgraph * add subscription id to query_microsoft_graph * migrating remaingin references * formatting * adding missing dependencies * flake fix * fix get_tenant_id * cleanup * formatting * migrate application creation in deploy.py * foramt * mypy fix * isort * isort * format * bug fixes * specify the correct signInAudience * fix backing service principal creation fix preauthorized application * remove remaining references to graphrbac * fix ms graph authentication * formatting * fix typo * format * deployment fix * set implicitGrantSettings in the deployment * format * fix deployment * fix graph authentication on the server * use the current cli logged in account to retrive the backend token cache * assign the the msgraph app role permissions to the web app during the deployment * formatting * fix build * build fix * fix bandit issue * mypy fix * isort * deploy fixes * formatting * remove assign_app_permissions * mypy fix * build fix * mypy fix * format * formatting * flake fix * remove webapp identity permission assignment * remove unused reference to assign_app_role * remove manual registration message * fixing name and logging * address PR coments * address PR comments * build fix * lint * lint * mypy fix * mypy fix * formatting * address PR comments * linting * lint * remove ONEFUZZ_AAD_GROUP_ID check * regenerate webhook_events.md * change return type of query_microsoft_graph_list * fix tenant_id Co-authored-by: Marc Greisen <marc@greisen.org> Co-authored-by: Stas <stishkin@live.com>
This commit is contained in:
@ -1004,7 +1004,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
||||
469,
|
||||
470,
|
||||
471,
|
||||
472
|
||||
472,
|
||||
473
|
||||
],
|
||||
"title": "ErrorCode"
|
||||
},
|
||||
@ -1615,7 +1616,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
||||
469,
|
||||
470,
|
||||
471,
|
||||
472
|
||||
472,
|
||||
473
|
||||
],
|
||||
"title": "ErrorCode"
|
||||
}
|
||||
@ -2501,7 +2503,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
||||
469,
|
||||
470,
|
||||
471,
|
||||
472
|
||||
472,
|
||||
473
|
||||
],
|
||||
"title": "ErrorCode"
|
||||
}
|
||||
@ -3190,7 +3193,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
||||
469,
|
||||
470,
|
||||
471,
|
||||
472
|
||||
472,
|
||||
473
|
||||
],
|
||||
"title": "ErrorCode"
|
||||
},
|
||||
@ -5123,7 +5127,8 @@ Each event will be submitted via HTTP POST to the user provided URL.
|
||||
469,
|
||||
470,
|
||||
471,
|
||||
472
|
||||
472,
|
||||
473
|
||||
],
|
||||
"title": "ErrorCode"
|
||||
},
|
||||
|
@ -6,12 +6,12 @@
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Callable, List, TypeVar, cast
|
||||
import urllib.parse
|
||||
from typing import Any, Callable, Dict, List, Optional, TypeVar, cast
|
||||
from uuid import UUID
|
||||
|
||||
import requests
|
||||
from azure.core.exceptions import ClientAuthenticationError
|
||||
from azure.graphrbac import GraphRbacManagementClient
|
||||
from azure.graphrbac.models import CheckGroupMembershipParameters
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from azure.keyvault.secrets import SecretClient
|
||||
from azure.mgmt.resource import ResourceManagementClient
|
||||
@ -23,6 +23,10 @@ from onefuzztypes.primitives import Container, Region
|
||||
|
||||
from .monkeypatch import allow_more_workers, reduce_logging
|
||||
|
||||
# https://docs.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0
|
||||
GRAPH_RESOURCE = "https://graph.microsoft.com"
|
||||
GRAPH_RESOURCE_ENDPOINT = "https://graph.microsoft.com/v1.0"
|
||||
|
||||
|
||||
@cached
|
||||
def get_msi() -> MSIAuthentication:
|
||||
@ -99,18 +103,77 @@ def get_regions() -> List[Region]:
|
||||
return sorted([Region(x.name) for x in locations])
|
||||
|
||||
|
||||
@cached
|
||||
def get_graph_client() -> GraphRbacManagementClient:
|
||||
return GraphRbacManagementClient(get_msi(), get_subscription())
|
||||
class GraphQueryError(Exception):
|
||||
def __init__(self, message: str, status_code: Optional[int]) -> None:
|
||||
super(GraphQueryError, self).__init__(message)
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def query_microsoft_graph(
|
||||
method: str,
|
||||
resource: str,
|
||||
params: Optional[Dict] = None,
|
||||
body: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
cred = get_identity()
|
||||
access_token = cred.get_token(f"{GRAPH_RESOURCE}/.default")
|
||||
|
||||
url = urllib.parse.urljoin(f"{GRAPH_RESOURCE_ENDPOINT}/", resource)
|
||||
headers = {
|
||||
"Authorization": "Bearer %s" % access_token.token,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
response = requests.request(
|
||||
method=method, url=url, headers=headers, params=params, json=body
|
||||
)
|
||||
|
||||
if 200 <= response.status_code < 300:
|
||||
if response.content and response.content.strip():
|
||||
json = response.json()
|
||||
if isinstance(json, Dict):
|
||||
return json
|
||||
else:
|
||||
raise GraphQueryError(
|
||||
"invalid data expected a json object: HTTP"
|
||||
f" {response.status_code} - {json}",
|
||||
response.status_code,
|
||||
)
|
||||
else:
|
||||
return {}
|
||||
else:
|
||||
error_text = str(response.content, encoding="utf-8", errors="backslashreplace")
|
||||
raise GraphQueryError(
|
||||
f"request did not succeed: HTTP {response.status_code} - {error_text}",
|
||||
response.status_code,
|
||||
)
|
||||
|
||||
|
||||
def query_microsoft_graph_list(
|
||||
method: str,
|
||||
resource: str,
|
||||
params: Optional[Dict] = None,
|
||||
body: Optional[Dict] = None,
|
||||
) -> List[Any]:
|
||||
result = query_microsoft_graph(
|
||||
method,
|
||||
resource,
|
||||
params,
|
||||
body,
|
||||
)
|
||||
value = result.get("value")
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
else:
|
||||
raise GraphQueryError("Expected data containing a list of values", None)
|
||||
|
||||
|
||||
def is_member_of(group_id: str, member_id: str) -> bool:
|
||||
client = get_graph_client()
|
||||
return bool(
|
||||
client.groups.is_member_of(
|
||||
CheckGroupMembershipParameters(group_id=group_id, member_id=member_id)
|
||||
).value
|
||||
body = {"groupIds": [group_id]}
|
||||
response = query_microsoft_graph_list(
|
||||
method="POST", resource=f"users/{member_id}/checkMemberGroups", body=body
|
||||
)
|
||||
return group_id in response
|
||||
|
||||
|
||||
@cached
|
||||
|
@ -10,13 +10,11 @@ from typing import TYPE_CHECKING, Optional, Sequence, Type, TypeVar, Union
|
||||
from uuid import UUID
|
||||
|
||||
from azure.functions import HttpRequest, HttpResponse
|
||||
from azure.graphrbac.models import GraphErrorException
|
||||
from onefuzztypes.enums import ErrorCode
|
||||
from onefuzztypes.models import Error
|
||||
from onefuzztypes.responses import BaseResponse
|
||||
from pydantic import ValidationError
|
||||
|
||||
from .azure.creds import is_member_of
|
||||
from .orm import ModelMixin
|
||||
|
||||
# We don't actually use these types at runtime at this time. Rather,
|
||||
@ -28,24 +26,14 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
def check_access(req: HttpRequest) -> Optional[Error]:
|
||||
if "ONEFUZZ_AAD_GROUP_ID" not in os.environ:
|
||||
return None
|
||||
|
||||
group_id = os.environ["ONEFUZZ_AAD_GROUP_ID"]
|
||||
member_id = req.headers["x-ms-client-principal-id"]
|
||||
try:
|
||||
result = is_member_of(group_id, member_id)
|
||||
except GraphErrorException:
|
||||
if "ONEFUZZ_AAD_GROUP_ID" in os.environ:
|
||||
message = "ONEFUZZ_AAD_GROUP_ID configuration not supported"
|
||||
logging.error(message)
|
||||
return Error(
|
||||
code=ErrorCode.UNAUTHORIZED, errors=["unable to interact with graph"]
|
||||
code=ErrorCode.INVALID_CONFIGURATION,
|
||||
errors=[message],
|
||||
)
|
||||
if not result:
|
||||
logging.error("unauthorized access: %s is not in %s", member_id, group_id)
|
||||
return Error(
|
||||
code=ErrorCode.UNAUTHORIZED,
|
||||
errors=["not approved to use this instance of onefuzz"],
|
||||
)
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
@ -4,7 +4,6 @@ azure-cosmosdb-nspkg==2.0.2
|
||||
azure-cosmosdb-table==1.0.6
|
||||
azure-devops==6.0.0b4
|
||||
azure-functions==1.7.2
|
||||
azure-graphrbac~=0.61.1
|
||||
azure-identity==1.6.1
|
||||
azure-keyvault-secrets~=4.3.0
|
||||
azure-mgmt-compute==22.0
|
||||
|
@ -22,18 +22,6 @@ from uuid import UUID
|
||||
from azure.common.client_factory import get_client_from_cli_profile
|
||||
from azure.common.credentials import get_cli_profile
|
||||
from azure.cosmosdb.table.tableservice import TableService
|
||||
from azure.graphrbac import GraphRbacManagementClient
|
||||
from azure.graphrbac.models import (
|
||||
Application,
|
||||
ApplicationCreateParameters,
|
||||
ApplicationUpdateParameters,
|
||||
AppRole,
|
||||
GraphErrorException,
|
||||
OptionalClaims,
|
||||
RequiredResourceAccess,
|
||||
ResourceAccess,
|
||||
ServicePrincipalCreateParameters,
|
||||
)
|
||||
from azure.mgmt.applicationinsights import ApplicationInsightsManagementClient
|
||||
from azure.mgmt.applicationinsights.models import (
|
||||
ApplicationInsightsComponentExportRequest,
|
||||
@ -61,11 +49,14 @@ from msrest.serialization import TZ_UTC
|
||||
|
||||
from data_migration import migrate
|
||||
from registration import (
|
||||
GraphQueryError,
|
||||
OnefuzzAppRole,
|
||||
add_application_password,
|
||||
assign_app_role,
|
||||
assign_instance_app_role,
|
||||
authorize_application,
|
||||
get_graph_client,
|
||||
get_application,
|
||||
get_tenant_id,
|
||||
query_microsoft_graph,
|
||||
register_application,
|
||||
set_app_audience,
|
||||
update_pool_registration,
|
||||
@ -249,7 +240,9 @@ class Client:
|
||||
sys.exit(1)
|
||||
|
||||
def create_password(self, object_id: UUID) -> Tuple[str, str]:
|
||||
return add_application_password(object_id, self.get_subscription_id())
|
||||
return add_application_password(
|
||||
"cli_password", object_id, self.get_subscription_id()
|
||||
)
|
||||
|
||||
def get_instance_url(self) -> str:
|
||||
# The url to access the instance
|
||||
@ -277,7 +270,14 @@ class Client:
|
||||
else:
|
||||
return "api://%s.azurewebsites.net" % self.application_name
|
||||
|
||||
def setup_rbac(self) -> None: # noqa: C901
|
||||
def get_signin_audience(self) -> str:
|
||||
# https://docs.microsoft.com/en-us/azure/active-directory/develop/supported-accounts-validation
|
||||
if self.multi_tenant_domain:
|
||||
return "AzureADMultipleOrgs"
|
||||
else:
|
||||
return "AzureADMyOrg"
|
||||
|
||||
def setup_rbac(self) -> None:
|
||||
"""
|
||||
Setup the client application for the OneFuzz instance.
|
||||
By default, Service Principals do not have access to create
|
||||
@ -287,78 +287,99 @@ class Client:
|
||||
logger.info("using existing client application")
|
||||
return
|
||||
|
||||
client = get_client_from_cli_profile(
|
||||
GraphRbacManagementClient, subscription_id=self.get_subscription_id()
|
||||
app = get_application(
|
||||
display_name=self.application_name,
|
||||
subscription_id=self.get_subscription_id(),
|
||||
)
|
||||
logger.info("checking if RBAC already exists")
|
||||
|
||||
try:
|
||||
existing = list(
|
||||
client.applications.list(
|
||||
filter="displayName eq '%s'" % self.application_name
|
||||
)
|
||||
)
|
||||
except GraphErrorException:
|
||||
logger.error("unable to query RBAC. Provide client_id and client_secret")
|
||||
sys.exit(1)
|
||||
|
||||
app_roles = [
|
||||
AppRole(
|
||||
allowed_member_types=["Application"],
|
||||
display_name=OnefuzzAppRole.CliClient.value,
|
||||
id=str(uuid.uuid4()),
|
||||
is_enabled=True,
|
||||
description="Allows access from the CLI.",
|
||||
value=OnefuzzAppRole.CliClient.value,
|
||||
),
|
||||
AppRole(
|
||||
allowed_member_types=["Application"],
|
||||
display_name=OnefuzzAppRole.ManagedNode.value,
|
||||
id=str(uuid.uuid4()),
|
||||
is_enabled=True,
|
||||
description="Allow access from a lab machine.",
|
||||
value=OnefuzzAppRole.ManagedNode.value,
|
||||
),
|
||||
{
|
||||
"allowedMemberTypes": ["Application"],
|
||||
"description": "Allows access from the CLI.",
|
||||
"displayName": OnefuzzAppRole.CliClient.value,
|
||||
"id": str(uuid.uuid4()),
|
||||
"isEnabled": True,
|
||||
"value": OnefuzzAppRole.CliClient.value,
|
||||
},
|
||||
{
|
||||
"allowedMemberTypes": ["Application"],
|
||||
"description": "Allow access from a lab machine.",
|
||||
"displayName": OnefuzzAppRole.ManagedNode.value,
|
||||
"id": str(uuid.uuid4()),
|
||||
"isEnabled": True,
|
||||
"value": OnefuzzAppRole.ManagedNode.value,
|
||||
},
|
||||
]
|
||||
|
||||
app: Optional[Application] = None
|
||||
|
||||
if not existing:
|
||||
if not app:
|
||||
logger.info("creating Application registration")
|
||||
|
||||
params = ApplicationCreateParameters(
|
||||
display_name=self.application_name,
|
||||
identifier_uris=[self.get_identifier_url()],
|
||||
reply_urls=[self.get_instance_url() + "/.auth/login/aad/callback"],
|
||||
optional_claims=OptionalClaims(id_token=[], access_token=[]),
|
||||
required_resource_access=[
|
||||
RequiredResourceAccess(
|
||||
resource_access=[
|
||||
ResourceAccess(id=USER_READ_PERMISSION, type="Scope")
|
||||
params = {
|
||||
"displayName": self.application_name,
|
||||
"identifierUris": [self.get_identifier_url()],
|
||||
"signInAudience": self.get_signin_audience(),
|
||||
"appRoles": app_roles,
|
||||
"api": {
|
||||
"oauth2PermissionScopes": [
|
||||
{
|
||||
"adminConsentDescription": f"Allow the application to access {self.application_name} on behalf of the signed-in user.",
|
||||
"adminConsentDisplayName": f"Access {self.application_name}",
|
||||
"id": str(uuid.uuid4()),
|
||||
"isEnabled": True,
|
||||
"type": "User",
|
||||
"userConsentDescription": f"Allow the application to access {self.application_name} on your behalf.",
|
||||
"userConsentDisplayName": f"Access {self.application_name}",
|
||||
"value": "user_impersonation",
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"implicitGrantSettings": {
|
||||
"enableAccessTokenIssuance": False,
|
||||
"enableIdTokenIssuance": True,
|
||||
},
|
||||
"redirectUris": [
|
||||
f"{self.get_instance_url()}/.auth/login/aad/callback"
|
||||
],
|
||||
resource_app_id=MICROSOFT_GRAPH_APP_ID,
|
||||
)
|
||||
},
|
||||
"requiredResourceAccess": [
|
||||
{
|
||||
"resourceAccess": [
|
||||
{"id": USER_READ_PERMISSION, "type": "Scope"}
|
||||
],
|
||||
app_roles=app_roles,
|
||||
)
|
||||
"resourceAppId": MICROSOFT_GRAPH_APP_ID,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
app = client.applications.create(params)
|
||||
app = query_microsoft_graph(
|
||||
method="POST",
|
||||
resource="applications",
|
||||
body=params,
|
||||
subscription=self.get_subscription_id(),
|
||||
)
|
||||
|
||||
logger.info("creating service principal")
|
||||
service_principal_params = ServicePrincipalCreateParameters(
|
||||
account_enabled=True,
|
||||
app_role_assignment_required=False,
|
||||
service_principal_type="Application",
|
||||
app_id=app.app_id,
|
||||
)
|
||||
|
||||
service_principal_params = {
|
||||
"accountEnabled": True,
|
||||
"appRoleAssignmentRequired": False,
|
||||
"servicePrincipalType": "Application",
|
||||
"appId": app["appId"],
|
||||
}
|
||||
|
||||
def try_sp_create() -> None:
|
||||
error: Optional[Exception] = None
|
||||
for _ in range(10):
|
||||
try:
|
||||
client.service_principals.create(service_principal_params)
|
||||
query_microsoft_graph(
|
||||
method="POST",
|
||||
resource="servicePrincipals",
|
||||
body=service_principal_params,
|
||||
subscription=self.get_subscription_id(),
|
||||
)
|
||||
return
|
||||
except GraphErrorException as err:
|
||||
except GraphQueryError as err:
|
||||
# work around timing issue when creating service principal
|
||||
# https://github.com/Azure/azure-cli/issues/14767
|
||||
if (
|
||||
@ -379,56 +400,70 @@ class Client:
|
||||
try_sp_create()
|
||||
|
||||
else:
|
||||
app = existing[0]
|
||||
existing_role_values = [app_role["value"] for app_role in app["appRoles"]]
|
||||
api_id = self.get_identifier_url()
|
||||
if api_id not in app.identifier_uris:
|
||||
identifier_uris = app.identifier_uris
|
||||
|
||||
if api_id not in app["identifierUris"]:
|
||||
identifier_uris = app["identifierUris"]
|
||||
identifier_uris.append(api_id)
|
||||
client.applications.patch(
|
||||
app.object_id,
|
||||
ApplicationUpdateParameters(identifier_uris=identifier_uris),
|
||||
query_microsoft_graph(
|
||||
method="PATCH",
|
||||
resource=f"applications/{app['id']}",
|
||||
body={"identifierUris": identifier_uris},
|
||||
subscription=self.get_subscription_id(),
|
||||
)
|
||||
|
||||
existing_role_values = [app_role.value for app_role in app.app_roles]
|
||||
has_missing_roles = any(
|
||||
[role.value not in existing_role_values for role in app_roles]
|
||||
[role["value"] not in existing_role_values for role in app_roles]
|
||||
)
|
||||
|
||||
if has_missing_roles:
|
||||
# disabling the existing app role first to allow the update
|
||||
# this is a requirement to update the application roles
|
||||
for role in app.app_roles:
|
||||
role.is_enabled = False
|
||||
for role in app["appRoles"]:
|
||||
role["isEnabled"] = False
|
||||
|
||||
client.applications.patch(
|
||||
app.object_id, ApplicationUpdateParameters(app_roles=app.app_roles)
|
||||
query_microsoft_graph(
|
||||
method="PATCH",
|
||||
resource=f"applications/{app['id']}",
|
||||
body={"appRoles": app["AppRoles"]},
|
||||
subscription=self.get_subscription_id(),
|
||||
)
|
||||
|
||||
# overriding the list of app roles
|
||||
client.applications.patch(
|
||||
app.object_id, ApplicationUpdateParameters(app_roles=app_roles)
|
||||
query_microsoft_graph(
|
||||
method="PATCH",
|
||||
resource=f"applications/{app['id']}",
|
||||
body={"appRoles": app_roles},
|
||||
subscription=self.get_subscription_id(),
|
||||
)
|
||||
|
||||
if self.multi_tenant_domain and app.sign_in_audience == "AzureADMyOrg":
|
||||
set_app_audience(app.object_id, "AzureADMultipleOrgs")
|
||||
if self.multi_tenant_domain and app["signInAudience"] == "AzureADMyOrg":
|
||||
set_app_audience(
|
||||
app["id"],
|
||||
"AzureADMultipleOrgs",
|
||||
subscription_id=self.get_subscription_id(),
|
||||
)
|
||||
elif (
|
||||
not self.multi_tenant_domain
|
||||
and app.sign_in_audience == "AzureADMultipleOrgs"
|
||||
and app["signInAudience"] == "AzureADMultipleOrgs"
|
||||
):
|
||||
set_app_audience(app.object_id, "AzureADMyOrg")
|
||||
set_app_audience(
|
||||
app["id"],
|
||||
"AzureADMyOrg",
|
||||
subscription_id=self.get_subscription_id(),
|
||||
)
|
||||
else:
|
||||
logger.debug("No change to App Registration signInAudence setting")
|
||||
|
||||
creds = list(client.applications.list_password_credentials(app.object_id))
|
||||
client.applications.update_password_credentials(app.object_id, creds)
|
||||
(password_id, password) = self.create_password(app["id"])
|
||||
|
||||
(password_id, password) = self.create_password(app.object_id)
|
||||
|
||||
cli_app = list(
|
||||
client.applications.list(filter="appId eq '%s'" % ONEFUZZ_CLI_APP)
|
||||
cli_app = get_application(
|
||||
app_id=uuid.UUID(ONEFUZZ_CLI_APP),
|
||||
subscription_id=self.get_subscription_id(),
|
||||
)
|
||||
|
||||
if len(cli_app) == 0:
|
||||
if not cli_app:
|
||||
logger.info(
|
||||
"Could not find the default CLI application under the current "
|
||||
"subscription, creating a new one"
|
||||
@ -449,22 +484,20 @@ class Client:
|
||||
}
|
||||
|
||||
else:
|
||||
onefuzz_cli_app = cli_app[0]
|
||||
authorize_application(uuid.UUID(onefuzz_cli_app.app_id), app.app_id)
|
||||
onefuzz_cli_app = cli_app
|
||||
authorize_application(uuid.UUID(onefuzz_cli_app["appId"]), app["appId"])
|
||||
if self.multi_tenant_domain:
|
||||
authority = COMMON_AUTHORITY
|
||||
else:
|
||||
onefuzz_client = get_graph_client(self.get_subscription_id())
|
||||
authority = (
|
||||
"https://login.microsoftonline.com/%s"
|
||||
% onefuzz_client.config.tenant_id
|
||||
)
|
||||
|
||||
tenant_id = get_tenant_id(self.get_subscription_id())
|
||||
authority = "https://login.microsoftonline.com/%s" % tenant_id
|
||||
self.cli_config = {
|
||||
"client_id": onefuzz_cli_app.app_id,
|
||||
"client_id": onefuzz_cli_app["appId"],
|
||||
"authority": authority,
|
||||
}
|
||||
|
||||
self.results["client_id"] = app.app_id
|
||||
self.results["client_id"] = app["appId"]
|
||||
self.results["client_secret"] = password
|
||||
|
||||
def deploy_template(self) -> None:
|
||||
@ -554,7 +587,7 @@ class Client:
|
||||
logger.info("Upgrading: skipping assignment of the managed identity role")
|
||||
return
|
||||
logger.info("assigning the user managed identity role")
|
||||
assign_app_role(
|
||||
assign_instance_app_role(
|
||||
self.application_name,
|
||||
self.results["deploy"]["scaleset-identity"]["value"],
|
||||
self.get_subscription_id(),
|
||||
|
@ -11,53 +11,40 @@ import urllib.parse
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple, TypeVar
|
||||
from uuid import UUID, uuid4
|
||||
from uuid import UUID
|
||||
|
||||
import requests
|
||||
from azure.cli.core.azclierror import AuthenticationError
|
||||
from azure.common.client_factory import get_client_from_cli_profile
|
||||
from azure.common.credentials import get_cli_profile
|
||||
from azure.graphrbac import GraphRbacManagementClient
|
||||
from azure.graphrbac.models import (
|
||||
Application,
|
||||
ApplicationCreateParameters,
|
||||
ApplicationUpdateParameters,
|
||||
AppRole,
|
||||
PasswordCredential,
|
||||
RequiredResourceAccess,
|
||||
ResourceAccess,
|
||||
ServicePrincipal,
|
||||
ServicePrincipalCreateParameters,
|
||||
)
|
||||
from functional import seq
|
||||
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")
|
||||
|
||||
## https://docs.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0
|
||||
GRAPH_RESOURCE = "https://graph.microsoft.com"
|
||||
GRAPH_RESOURCE_ENDPOINT = "https://graph.microsoft.com/v1.0"
|
||||
|
||||
|
||||
class GraphQueryError(Exception):
|
||||
def __init__(self, message: str, status_code: int) -> None:
|
||||
def __init__(self, message: str, status_code: Optional[int]) -> None:
|
||||
super(GraphQueryError, self).__init__(message)
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
## Queries microsoft graph api and return
|
||||
def query_microsoft_graph(
|
||||
method: str,
|
||||
resource: str,
|
||||
params: Optional[Dict] = None,
|
||||
body: Optional[Dict] = None,
|
||||
) -> Any:
|
||||
subscription: Optional[str] = None,
|
||||
) -> Dict:
|
||||
profile = get_cli_profile()
|
||||
(token_type, access_token, _), _, _ = profile.get_raw_token(
|
||||
resource="https://graph.microsoft.com"
|
||||
resource=GRAPH_RESOURCE, subscription=subscription
|
||||
)
|
||||
url = urllib.parse.urljoin("https://graph.microsoft.com/v1.0/", resource)
|
||||
url = urllib.parse.urljoin(f"{GRAPH_RESOURCE_ENDPOINT}/", resource)
|
||||
headers = {
|
||||
"Authorization": "%s %s" % (token_type, access_token),
|
||||
"Content-Type": "application/json",
|
||||
@ -65,23 +52,60 @@ def query_microsoft_graph(
|
||||
response = requests.request(
|
||||
method=method, url=url, headers=headers, params=params, json=body
|
||||
)
|
||||
|
||||
response.status_code
|
||||
|
||||
if 200 <= response.status_code < 300:
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError:
|
||||
return None
|
||||
if response.content and response.content.strip():
|
||||
json = response.json()
|
||||
if isinstance(json, Dict):
|
||||
return json
|
||||
else:
|
||||
raise GraphQueryError(
|
||||
f"invalid data received expected a json object: HTTP {response.status_code} - {json}",
|
||||
response.status_code,
|
||||
)
|
||||
else:
|
||||
return {}
|
||||
else:
|
||||
error_text = str(response.content, encoding="utf-8", errors="backslashreplace")
|
||||
raise GraphQueryError(
|
||||
"request did not succeed: HTTP %s - %s"
|
||||
% (response.status_code, error_text),
|
||||
f"request did not succeed: HTTP {response.status_code} - {error_text}",
|
||||
response.status_code,
|
||||
)
|
||||
|
||||
|
||||
def query_microsoft_graph_list(
|
||||
method: str,
|
||||
resource: str,
|
||||
params: Optional[Dict] = None,
|
||||
body: Optional[Dict] = None,
|
||||
subscription: Optional[str] = None,
|
||||
) -> List[Any]:
|
||||
result = query_microsoft_graph(
|
||||
method,
|
||||
resource,
|
||||
params,
|
||||
body,
|
||||
subscription,
|
||||
)
|
||||
value = result.get("value")
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
else:
|
||||
raise GraphQueryError("Expected data containing a list of values", None)
|
||||
|
||||
|
||||
def get_tenant_id(subscription_id: Optional[str] = None) -> str:
|
||||
profile = get_cli_profile()
|
||||
_, _, tenant_id = profile.get_raw_token(
|
||||
resource=GRAPH_RESOURCE, subscription=subscription_id
|
||||
)
|
||||
if isinstance(tenant_id, str):
|
||||
return tenant_id
|
||||
else:
|
||||
raise Exception(
|
||||
f"unable to retrive tenant_id for subscription {subscription_id}"
|
||||
)
|
||||
|
||||
|
||||
OperationResult = TypeVar("OperationResult")
|
||||
|
||||
|
||||
@ -121,14 +145,6 @@ def retry(
|
||||
time.sleep(wait_duration)
|
||||
|
||||
|
||||
def get_graph_client(subscription_id: str) -> GraphRbacManagementClient:
|
||||
client: GraphRbacManagementClient = get_client_from_cli_profile(
|
||||
GraphRbacManagementClient,
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
return client
|
||||
|
||||
|
||||
class ApplicationInfo(NamedTuple):
|
||||
client_id: UUID
|
||||
client_secret: str
|
||||
@ -147,46 +163,41 @@ def register_application(
|
||||
subscription_id: str,
|
||||
) -> ApplicationInfo:
|
||||
logger.info("retrieving the application registration %s" % registration_name)
|
||||
client = get_graph_client(subscription_id)
|
||||
apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % registration_name)
|
||||
|
||||
app = get_application(
|
||||
display_name=registration_name, subscription_id=subscription_id
|
||||
)
|
||||
|
||||
if len(apps) == 0:
|
||||
if not app:
|
||||
logger.info("No existing registration found. creating a new one")
|
||||
app = create_application_registration(
|
||||
onefuzz_instance_name, registration_name, approle, subscription_id
|
||||
)
|
||||
else:
|
||||
app = apps[0]
|
||||
logger.info(
|
||||
"Found existing application objectId '%s' - appid '%s'"
|
||||
% (app.object_id, app.app_id)
|
||||
% (app["id"], app["appId"])
|
||||
)
|
||||
|
||||
onefuzz_apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
||||
onefuzz_app = get_application(
|
||||
display_name=onefuzz_instance_name, subscription_id=subscription_id
|
||||
)
|
||||
|
||||
if len(onefuzz_apps) == 0:
|
||||
if not (onefuzz_app):
|
||||
raise Exception("onefuzz app not found")
|
||||
|
||||
onefuzz_app = onefuzz_apps[0]
|
||||
pre_authorized_applications = (
|
||||
onefuzz_app.pre_authorized_applications
|
||||
if onefuzz_app.pre_authorized_applications is not None
|
||||
else []
|
||||
)
|
||||
pre_authorized_applications = onefuzz_app["api"]["preAuthorizedApplications"]
|
||||
|
||||
if app.app_id not in [app.app_id for app in pre_authorized_applications]:
|
||||
authorize_application(UUID(app.app_id), UUID(onefuzz_app.app_id))
|
||||
if app["appId"] not in [app["appId"] for app in pre_authorized_applications]:
|
||||
authorize_application(UUID(app["appId"]), UUID(onefuzz_app["appId"]))
|
||||
|
||||
password = create_application_credential(registration_name, subscription_id)
|
||||
tenant_id = get_tenant_id(subscription_id=subscription_id)
|
||||
|
||||
return ApplicationInfo(
|
||||
client_id=app.app_id,
|
||||
client_id=app["appId"],
|
||||
client_secret=password,
|
||||
authority=("https://login.microsoftonline.com/%s" % client.config.tenant_id),
|
||||
authority=("https://login.microsoftonline.com/%s" % tenant_id),
|
||||
)
|
||||
|
||||
|
||||
@ -194,93 +205,93 @@ def create_application_credential(application_name: str, subscription_id: str) -
|
||||
"""Add a new password to the application registration"""
|
||||
|
||||
logger.info("creating application credential for '%s'" % application_name)
|
||||
client = get_graph_client(subscription_id)
|
||||
apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % application_name)
|
||||
app = get_application(display_name=application_name)
|
||||
|
||||
if not app:
|
||||
raise Exception("app not found")
|
||||
|
||||
(_, password) = add_application_password(
|
||||
f"{application_name}_password", app["id"], subscription_id
|
||||
)
|
||||
|
||||
app: Application = apps[0]
|
||||
|
||||
(_, password) = add_application_password(app.object_id, subscription_id)
|
||||
return str(password)
|
||||
|
||||
|
||||
def create_application_registration(
|
||||
onefuzz_instance_name: str, name: str, approle: OnefuzzAppRole, subscription_id: str
|
||||
) -> Application:
|
||||
) -> Any:
|
||||
"""Create an application registration"""
|
||||
|
||||
client = get_graph_client(subscription_id)
|
||||
apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
||||
app = get_application(
|
||||
display_name=onefuzz_instance_name, subscription_id=subscription_id
|
||||
)
|
||||
|
||||
app = apps[0]
|
||||
if not app:
|
||||
raise Exception("onefuzz app registration not found")
|
||||
|
||||
resource_access = [
|
||||
ResourceAccess(id=role.id, type="Role")
|
||||
for role in app.app_roles
|
||||
if role.value == approle.value
|
||||
{"id": role["id"], "type": "Scope"}
|
||||
for role in app["appRoles"]
|
||||
if role["value"] == approle.value
|
||||
]
|
||||
|
||||
params = ApplicationCreateParameters(
|
||||
is_device_only_auth_supported=True,
|
||||
display_name=name,
|
||||
identifier_uris=[],
|
||||
password_credentials=[],
|
||||
required_resource_access=(
|
||||
params = {
|
||||
"isDeviceOnlyAuthSupported": True,
|
||||
"displayName": name,
|
||||
"publicClient": {
|
||||
"redirectUris": ["https://%s.azurewebsites.net" % onefuzz_instance_name]
|
||||
},
|
||||
"isFallbackPublicClient": True,
|
||||
"requiredResourceAccess": (
|
||||
[
|
||||
RequiredResourceAccess(
|
||||
resource_access=resource_access,
|
||||
resource_app_id=app.app_id,
|
||||
)
|
||||
{
|
||||
"resourceAccess": resource_access,
|
||||
"resourceAppId": app["appId"],
|
||||
}
|
||||
]
|
||||
if len(resource_access) > 0
|
||||
else []
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
registered_app: Application = client.applications.create(params)
|
||||
registered_app = query_microsoft_graph(
|
||||
method="POST",
|
||||
resource="applications",
|
||||
body=params,
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
logger.info("creating service principal")
|
||||
service_principal_params = ServicePrincipalCreateParameters(
|
||||
account_enabled=True,
|
||||
app_role_assignment_required=False,
|
||||
service_principal_type="Application",
|
||||
app_id=registered_app.app_id,
|
||||
|
||||
service_principal_params = {
|
||||
"accountEnabled": True,
|
||||
"appRoleAssignmentRequired": False,
|
||||
"servicePrincipalType": "Application",
|
||||
"appId": registered_app["appId"],
|
||||
}
|
||||
|
||||
query_microsoft_graph(
|
||||
method="POST",
|
||||
resource="servicePrincipals",
|
||||
body=service_principal_params,
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
client.service_principals.create(service_principal_params)
|
||||
|
||||
atttempts = 5
|
||||
while True:
|
||||
if atttempts < 0:
|
||||
raise Exception(
|
||||
"Unable to create application registration, Please try again"
|
||||
authorize_application(
|
||||
UUID(registered_app["appId"]),
|
||||
UUID(app["appId"]),
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
|
||||
atttempts = atttempts - 1
|
||||
try:
|
||||
time.sleep(5)
|
||||
|
||||
update_param = ApplicationUpdateParameters(
|
||||
reply_urls=["https://%s.azurewebsites.net" % onefuzz_instance_name]
|
||||
)
|
||||
client.applications.patch(registered_app.object_id, update_param)
|
||||
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
authorize_application(UUID(registered_app.app_id), UUID(app.app_id))
|
||||
assign_app_role(onefuzz_instance_name, name, subscription_id, approle)
|
||||
assign_instance_app_role(onefuzz_instance_name, name, subscription_id, approle)
|
||||
return registered_app
|
||||
|
||||
|
||||
def add_application_password(
|
||||
app_object_id: UUID, subscription_id: str
|
||||
password_name: str, app_object_id: UUID, subscription_id: str
|
||||
) -> Tuple[str, str]:
|
||||
def create_password(data: Any) -> Tuple[str, str]:
|
||||
password = add_application_password_impl(app_object_id, subscription_id)
|
||||
password = add_application_password_impl(
|
||||
password_name, app_object_id, subscription_id
|
||||
)
|
||||
logger.info("app password created")
|
||||
return password
|
||||
|
||||
@ -289,35 +300,32 @@ def add_application_password(
|
||||
return retry(create_password, "create password")
|
||||
|
||||
|
||||
def add_application_password_legacy(
|
||||
app_object_id: UUID, subscription_id: str
|
||||
) -> Tuple[str, str]:
|
||||
key = str(uuid4())
|
||||
password = str(uuid4())
|
||||
|
||||
client = get_graph_client(subscription_id)
|
||||
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, subscription_id: str
|
||||
password_name: str, app_object_id: UUID, subscription_id: str
|
||||
) -> Tuple[str, str]:
|
||||
key = uuid4()
|
||||
|
||||
app = query_microsoft_graph(
|
||||
method="GET",
|
||||
resource="applications/%s" % app_object_id,
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
passwords = [
|
||||
x for x in app["passwordCredentials"] if x["displayName"] == password_name
|
||||
]
|
||||
|
||||
if len(passwords) > 0:
|
||||
key_id = passwords[0]["keyId"]
|
||||
query_microsoft_graph(
|
||||
method="POST",
|
||||
resource="applications/%s/removePassword" % app_object_id,
|
||||
body={"keyId": key_id},
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
password_request = {
|
||||
"passwordCredential": {
|
||||
"displayName": "%s" % key,
|
||||
"displayName": "%s" % password_name,
|
||||
"startDateTime": "%s" % datetime.now(TZ_UTC).strftime("%Y-%m-%dT%H:%M.%fZ"),
|
||||
"endDateTime": "%s"
|
||||
% (datetime.now(TZ_UTC) + timedelta(days=365)).strftime(
|
||||
@ -326,36 +334,56 @@ def add_application_password_impl(
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
password: Dict = query_microsoft_graph(
|
||||
method="POST",
|
||||
resource="applications/%s/addPassword" % app_object_id,
|
||||
body=password_request,
|
||||
subscription=subscription_id,
|
||||
)
|
||||
return (str(key), password["secretText"])
|
||||
except AuthenticationError:
|
||||
return add_application_password_legacy(app_object_id, subscription_id)
|
||||
return (password_name, password["secretText"])
|
||||
|
||||
|
||||
def get_application(app_id: UUID) -> Optional[Any]:
|
||||
apps: Dict = query_microsoft_graph(
|
||||
def get_application(
|
||||
app_id: Optional[UUID] = None,
|
||||
display_name: Optional[str] = None,
|
||||
subscription_id: Optional[str] = None,
|
||||
) -> Optional[Any]:
|
||||
filters = []
|
||||
if app_id:
|
||||
filters.append("appId eq '%s'" % app_id)
|
||||
if display_name:
|
||||
filters.append("displayName eq '%s'" % display_name)
|
||||
|
||||
filter_str = " and ".join(filters)
|
||||
|
||||
apps = query_microsoft_graph(
|
||||
method="GET",
|
||||
resource="applications",
|
||||
params={"$filter": "appId eq '%s'" % app_id},
|
||||
params={
|
||||
"$filter": filter_str,
|
||||
},
|
||||
subscription=subscription_id,
|
||||
)
|
||||
if len(apps["value"]) == 0:
|
||||
number_of_apps = len(apps["value"])
|
||||
if number_of_apps == 0:
|
||||
return None
|
||||
|
||||
elif number_of_apps == 1:
|
||||
return apps["value"][0]
|
||||
else:
|
||||
raise Exception(
|
||||
f"Found {number_of_apps} application matching filter: '{filter_str}'"
|
||||
)
|
||||
|
||||
|
||||
def authorize_application(
|
||||
registration_app_id: UUID,
|
||||
onefuzz_app_id: UUID,
|
||||
permissions: List[str] = ["user_impersonation"],
|
||||
subscription_id: Optional[str] = None,
|
||||
) -> None:
|
||||
try:
|
||||
onefuzz_app = get_application(onefuzz_app_id)
|
||||
onefuzz_app = get_application(
|
||||
app_id=onefuzz_app_id, subscription_id=subscription_id
|
||||
)
|
||||
if onefuzz_app is None:
|
||||
logger.error("Application '%s' not found", onefuzz_app_id)
|
||||
return
|
||||
@ -390,6 +418,7 @@ def authorize_application(
|
||||
method="PATCH",
|
||||
resource="applications/%s" % onefuzz_app_id,
|
||||
body={"api": {"preAuthorizedApplications": app_list}},
|
||||
subscription=subscription_id,
|
||||
)
|
||||
except GraphQueryError as e:
|
||||
m = re.search(
|
||||
@ -415,9 +444,6 @@ def authorize_application(
|
||||
"authorize application",
|
||||
data=preAuthorizedApplications.to_list(),
|
||||
)
|
||||
except AuthenticationError:
|
||||
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(
|
||||
@ -451,70 +477,63 @@ def update_pool_registration(onefuzz_instance_name: str, subscription_id: str) -
|
||||
)
|
||||
|
||||
|
||||
def assign_app_role_manually(
|
||||
onefuzz_instance_name: str,
|
||||
application_name: str,
|
||||
def assign_app_role(
|
||||
principal_id: str,
|
||||
application_id: str,
|
||||
role_names: List[str],
|
||||
subscription_id: str,
|
||||
app_role: OnefuzzAppRole,
|
||||
) -> None:
|
||||
application_registrations = query_microsoft_graph_list(
|
||||
method="GET",
|
||||
resource="servicePrincipals",
|
||||
params={
|
||||
"$filter": f"appId eq '{application_id}'",
|
||||
},
|
||||
subscription=subscription_id,
|
||||
)
|
||||
if len(application_registrations) == 0:
|
||||
raise Exception(f"appid '{application_id}' was not found:")
|
||||
app = application_registrations[0]
|
||||
|
||||
client = get_graph_client(subscription_id)
|
||||
apps: List[Application] = list(
|
||||
client.applications.list(filter="displayName eq '%s'" % onefuzz_instance_name)
|
||||
roles = (
|
||||
seq(app["appRoles"]).filter(lambda role: role["value"] in role_names).to_list()
|
||||
)
|
||||
|
||||
if not apps:
|
||||
raise Exception("onefuzz app registration not found")
|
||||
|
||||
app = apps[0]
|
||||
appId = app.app_id
|
||||
|
||||
onefuzz_service_principals: List[ServicePrincipal] = list(
|
||||
client.service_principals.list(filter="appId eq '%s'" % appId)
|
||||
if len(roles) < len(role_names):
|
||||
existing_roles = [role["value"] for role in roles]
|
||||
missing_roles = [
|
||||
role_name for role_name in role_names if role_name not in existing_roles
|
||||
]
|
||||
raise Exception(
|
||||
f"The following roles could not be found in appId '{application_id}': {missing_roles}"
|
||||
)
|
||||
|
||||
if not onefuzz_service_principals:
|
||||
raise Exception("onefuzz app service principal not found")
|
||||
onefuzz_service_principal = onefuzz_service_principals[0]
|
||||
|
||||
scaleset_service_principals: List[ServicePrincipal] = list(
|
||||
client.service_principals.list(filter="displayName eq '%s'" % application_name)
|
||||
expected_role_ids = [role["id"] for role in roles]
|
||||
assignments = query_microsoft_graph_list(
|
||||
method="GET",
|
||||
resource=f"servicePrincipals/{principal_id}/appRoleAssignments",
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
if not scaleset_service_principals:
|
||||
raise Exception("scaleset service principal not found")
|
||||
scaleset_service_principal = scaleset_service_principals[0]
|
||||
|
||||
scaleset_service_principal.app_roles
|
||||
app_roles: List[AppRole] = [
|
||||
role for role in app.app_roles if role.value == app_role.value
|
||||
assigned_role_ids = [assignment["appRoleId"] for assignment in assignments]
|
||||
missing_assignments = [
|
||||
id for id in expected_role_ids if id not in assigned_role_ids
|
||||
]
|
||||
|
||||
if not app_roles:
|
||||
raise Exception(
|
||||
"ManagedNode role not found in the OneFuzz application "
|
||||
"registration. Please redeploy the instance"
|
||||
)
|
||||
|
||||
body = '{ "principalId": "%s", "resourceId": "%s", "appRoleId": "%s"}' % (
|
||||
scaleset_service_principal.object_id,
|
||||
onefuzz_service_principal.object_id,
|
||||
app_roles[0].id,
|
||||
)
|
||||
|
||||
query = (
|
||||
"az rest --method post --url "
|
||||
"https://graph.microsoft.com/v1.0/servicePrincipals/%s/appRoleAssignedTo "
|
||||
"--body '%s' --headers \"Content-Type\"=application/json"
|
||||
% (scaleset_service_principal.object_id, body)
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
"execute the following query in the azure portal bash shell : \n%s" % query
|
||||
if missing_assignments:
|
||||
for app_role_id in missing_assignments:
|
||||
query_microsoft_graph(
|
||||
method="POST",
|
||||
resource=f"servicePrincipals/{principal_id}/appRoleAssignedTo",
|
||||
body={
|
||||
"principalId": principal_id,
|
||||
"resourceId": app["id"],
|
||||
"appRoleId": app_role_id,
|
||||
},
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
|
||||
def assign_app_role(
|
||||
def assign_instance_app_role(
|
||||
onefuzz_instance_name: str,
|
||||
application_name: str,
|
||||
subscription_id: str,
|
||||
@ -524,35 +543,38 @@ def assign_app_role(
|
||||
Allows the application to access the service by assigning
|
||||
their managed identity to the provided App Role
|
||||
"""
|
||||
try:
|
||||
onefuzz_service_appId = query_microsoft_graph(
|
||||
|
||||
onefuzz_service_appIds = query_microsoft_graph_list(
|
||||
method="GET",
|
||||
resource="applications",
|
||||
params={
|
||||
"$filter": "displayName eq '%s'" % onefuzz_instance_name,
|
||||
"$select": "appId",
|
||||
},
|
||||
subscription=subscription_id,
|
||||
)
|
||||
if len(onefuzz_service_appId["value"]) == 0:
|
||||
if len(onefuzz_service_appIds) == 0:
|
||||
raise Exception("onefuzz app registration not found")
|
||||
appId = onefuzz_service_appId["value"][0]["appId"]
|
||||
onefuzz_service_principals = query_microsoft_graph(
|
||||
appId = onefuzz_service_appIds[0]["appId"]
|
||||
onefuzz_service_principals = query_microsoft_graph_list(
|
||||
method="GET",
|
||||
resource="servicePrincipals",
|
||||
params={"$filter": "appId eq '%s'" % appId},
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
if len(onefuzz_service_principals["value"]) == 0:
|
||||
if len(onefuzz_service_principals) == 0:
|
||||
raise Exception("onefuzz app service principal not found")
|
||||
onefuzz_service_principal = onefuzz_service_principals["value"][0]
|
||||
scaleset_service_principals = query_microsoft_graph(
|
||||
onefuzz_service_principal = onefuzz_service_principals[0]
|
||||
application_service_principals = query_microsoft_graph_list(
|
||||
method="GET",
|
||||
resource="servicePrincipals",
|
||||
params={"$filter": "displayName eq '%s'" % application_name},
|
||||
subscription=subscription_id,
|
||||
)
|
||||
if len(scaleset_service_principals["value"]) == 0:
|
||||
raise Exception("scaleset service principal not found")
|
||||
scaleset_service_principal = scaleset_service_principals["value"][0]
|
||||
if len(application_service_principals) == 0:
|
||||
raise Exception(f"application '{application_name}' service principal not found")
|
||||
application_service_principal = application_service_principals[0]
|
||||
managed_node_role = (
|
||||
seq(onefuzz_service_principal["appRoles"])
|
||||
.filter(lambda x: x["value"] == app_role.value)
|
||||
@ -564,34 +586,34 @@ def assign_app_role(
|
||||
f"{app_role.value} role not found in the OneFuzz application "
|
||||
"registration. Please redeploy the instance"
|
||||
)
|
||||
assignments = query_microsoft_graph(
|
||||
assignments = query_microsoft_graph_list(
|
||||
method="GET",
|
||||
resource="servicePrincipals/%s/appRoleAssignments"
|
||||
% scaleset_service_principal["id"],
|
||||
% application_service_principal["id"],
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
# check if the role is already assigned
|
||||
role_assigned = seq(assignments["value"]).find(
|
||||
role_assigned = seq(assignments).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"],
|
||||
% application_service_principal["id"],
|
||||
body={
|
||||
"principalId": scaleset_service_principal["id"],
|
||||
"principalId": application_service_principal["id"],
|
||||
"resourceId": onefuzz_service_principal["id"],
|
||||
"appRoleId": managed_node_role["id"],
|
||||
},
|
||||
)
|
||||
except AuthenticationError:
|
||||
assign_app_role_manually(
|
||||
onefuzz_instance_name, application_name, subscription_id, app_role
|
||||
subscription=subscription_id,
|
||||
)
|
||||
|
||||
|
||||
def set_app_audience(objectId: str, audience: str) -> None:
|
||||
def set_app_audience(
|
||||
objectId: str, audience: str, subscription_id: Optional[str] = None
|
||||
) -> None:
|
||||
# typical audience values: AzureADMyOrg, AzureADMultipleOrgs
|
||||
http_body = {"signInAudience": audience}
|
||||
try:
|
||||
@ -599,6 +621,7 @@ def set_app_audience(objectId: str, audience: str) -> None:
|
||||
method="PATCH",
|
||||
resource="applications/%s" % objectId,
|
||||
body=http_body,
|
||||
subscription=subscription_id,
|
||||
)
|
||||
except GraphQueryError:
|
||||
query = (
|
||||
@ -680,7 +703,7 @@ def main() -> None:
|
||||
display_secret=True,
|
||||
)
|
||||
elif args.command == "assign_scaleset_role":
|
||||
assign_app_role(
|
||||
assign_instance_app_role(
|
||||
onefuzz_instance_name,
|
||||
args.scaleset_name,
|
||||
args.subscription_id,
|
||||
|
@ -265,6 +265,7 @@ class ErrorCode(Enum):
|
||||
NOTIFICATION_FAILURE = 470
|
||||
UNABLE_TO_UPDATE = 471
|
||||
PROXY_FAILED = 472
|
||||
INVALID_CONFIGURATION = 473
|
||||
|
||||
|
||||
class HeartbeatType(Enum):
|
||||
|
Reference in New Issue
Block a user