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:
Cheick Keita
2021-10-22 11:59:05 -07:00
committed by GitHub
parent c97395a37f
commit 98cd7c9c56
9 changed files with 586 additions and 474 deletions

View File

@ -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"
},

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(),

View File

@ -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,

View File

@ -265,6 +265,7 @@ class ErrorCode(Enum):
NOTIFICATION_FAILURE = 470
UNABLE_TO_UPDATE = 471
PROXY_FAILED = 472
INVALID_CONFIGURATION = 473
class HeartbeatType(Enum):