diff --git a/src/deployment/deploy.py b/src/deployment/deploy.py index 251d27a362..c8715ef863 100644 --- a/src/deployment/deploy.py +++ b/src/deployment/deploy.py @@ -60,9 +60,12 @@ GraphQueryError, OnefuzzAppRole, add_application_password, + add_user, assign_instance_app_role, authorize_application, get_application, + get_service_principal, + get_signed_in_user, get_tenant_id, query_microsoft_graph, register_application, @@ -300,7 +303,6 @@ def setup_rbac(self) -> None: display_name=self.application_name, subscription_id=self.get_subscription_id(), ) - app_roles = [ { "allowedMemberTypes": ["Application"], @@ -318,6 +320,14 @@ def setup_rbac(self) -> None: "isEnabled": True, "value": OnefuzzAppRole.ManagedNode.value, }, + { + "allowedMemberTypes": ["User"], + "description": "Allows user access from the CLI.", + "displayName": OnefuzzAppRole.UserAssignment.value, + "id": str(uuid.uuid4()), + "isEnabled": True, + "value": OnefuzzAppRole.UserAssignment.value, + }, ] if not app: @@ -372,7 +382,7 @@ def setup_rbac(self) -> None: service_principal_params = { "accountEnabled": True, - "appRoleAssignmentRequired": False, + "appRoleAssignmentRequired": True, "servicePrincipalType": "Application", "appId": app["appId"], } @@ -431,11 +441,10 @@ def try_sp_create() -> None: # this is a requirement to update the application roles for role in app["appRoles"]: role["isEnabled"] = False - query_microsoft_graph( method="PATCH", resource=f"applications/{app['id']}", - body={"appRoles": app["AppRoles"]}, + body={"appRoles": app["appRoles"]}, subscription=self.get_subscription_id(), ) @@ -603,6 +612,37 @@ def assign_scaleset_identity_role(self) -> None: OnefuzzAppRole.ManagedNode, ) + def assign_user_access(self) -> None: + logger.info("assinging user access to service principal") + app = get_application( + display_name=self.application_name, + subscription_id=self.get_subscription_id(), + ) + user = get_signed_in_user(self.subscription_id) + + if app: + sp = get_service_principal(app["appId"], self.subscription_id) + # Update appRoleAssignmentRequired if necessary + if not sp["appRoleAssignmentRequired"]: + logger.warning( + "The service is not currently configured to require a role assignment to access it." + + " This means that any authenticated user can access the service. " + + " To change this behavior enable 'Assignment Required?' on the service principal in the AAD Portal." + ) + + # Assign Roles and Add Users + roles = [ + x["id"] + for x in app["appRoles"] + if x["displayName"] == OnefuzzAppRole.UserAssignment.value + ] + users = [user["id"]] + if self.admins: + admins_str = [str(x) for x in self.admins] + users += admins_str + for user_id in users: + add_user(sp["id"], user_id, roles[0]) + def apply_migrations(self) -> None: logger.info("applying database migrations") name = self.results["deploy"]["func-name"]["value"] @@ -983,6 +1023,7 @@ def main() -> None: ("rbac", Client.setup_rbac), ("arm", Client.deploy_template), ("assign_scaleset_identity_role", Client.assign_scaleset_identity_role), + ("assign_user_access", Client.assign_user_access), ] full_deployment_states = rbac_only_states + [ diff --git a/src/deployment/deploylib/registration.py b/src/deployment/deploylib/registration.py index dad342ff8d..78eebc92e3 100644 --- a/src/deployment/deploylib/registration.py +++ b/src/deployment/deploylib/registration.py @@ -154,6 +154,7 @@ class ApplicationInfo(NamedTuple): class OnefuzzAppRole(Enum): ManagedNode = "ManagedNode" CliClient = "CliClient" + UserAssignment = "UserAssignment" def register_application( @@ -646,6 +647,84 @@ def set_app_audience( raise Exception(err_str) +def get_signed_in_user(subscription_id: Optional[str]) -> Any: + # Get principalId by retrieving owner for SP + try: + app = query_microsoft_graph( + method="GET", + resource="me/", + subscription=subscription_id, + ) + return app + + except GraphQueryError: + query = ( + "az rest --method post --url " + "https://graph.microsoft.com/v1.0/me " + '--headers "Content-Type"=application/json' + ) + logger.warning( + "execute the following query in the azure portal bash shell and " + "run deploy.py again : \n%s", + query, + ) + err_str = "Unable to retrieve signed-in user via Microsoft Graph Query API. \n" + raise Exception(err_str) + + +def get_service_principal(app_id: str, subscription_id: Optional[str]) -> Any: + try: + service_principals = query_microsoft_graph_list( + method="GET", + resource="servicePrincipals", + params={"$filter": f"appId eq '{app_id}'"}, + subscription=subscription_id, + ) + if len(service_principals) != 0: + return service_principals[0] + else: + raise GraphQueryError( + f"Could not retrieve any service principals for App Id: {app_id}", 400 + ) + + except GraphQueryError: + err_str = "Unable to add retrieve SP using Microsoft Graph Query API. \n" + raise Exception(err_str) + + +def add_user(object_id: str, principal_id: str, role_id: str) -> None: + # Get principalId by retrieving owner for SP + # need to add users with proper role assignment + http_body = { + "principalId": principal_id, + "resourceId": object_id, + "appRoleId": role_id, + } + try: + query_microsoft_graph( + method="POST", + resource="users/%s/appRoleAssignments" % principal_id, + body=http_body, + ) + except GraphQueryError as ex: + if "Permission being assigned already exists" not in ex.message: + query = ( + "az rest --method post --url " + "https://graph.microsoft.com/v1.0/users/%s/appRoleAssignments " + "--body '%s' --headers \"Content-Type\"=application/json" + % (principal_id, http_body) + ) + logger.warning( + "execute the following query in the azure portal bash shell and " + "run deploy.py again : \n%s", + query, + ) + err_str = "Unable to add user to SP using Microsoft Graph Query API. \n" + raise Exception(err_str) + else: + logger.info("User already assigned to application.") + + def main() -> None: formatter = argparse.ArgumentDefaultsHelpFormatter diff --git a/src/deployment/deploylib/tests/test_deploy_config.py b/src/deployment/deploylib/tests/test_deploy_config.py index 6e39780ed6..dc0bdf8521 100644 --- a/src/deployment/deploylib/tests/test_deploy_config.py +++ b/src/deployment/deploylib/tests/test_deploy_config.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. import unittest -from typing import Any, List +from typing import Any from deploylib.configuration import NetworkSecurityConfig