From d09f75a737f2142b66f85cfd563618085f46c0e3 Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Wed, 6 Aug 2025 15:03:30 -0400 Subject: [PATCH 1/7] Changing auth for ssh --- src/ssh/azext_ssh/connectivity_utils.py | 32 ++++++++++++++--- src/ssh/azext_ssh/constants.py | 4 +-- src/ssh/azext_ssh/custom.py | 35 ++++++++++++++++--- .../tests/latest/test_connectivity_utils.py | 11 ++++-- src/ssh/azext_ssh/tests/latest/test_custom.py | 12 +++++-- src/ssh/setup.py | 2 +- 6 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/ssh/azext_ssh/connectivity_utils.py b/src/ssh/azext_ssh/connectivity_utils.py index cd7596d2da3..1ba9b1402b8 100644 --- a/src/ssh/azext_ssh/connectivity_utils.py +++ b/src/ssh/azext_ssh/connectivity_utils.py @@ -219,7 +219,7 @@ def _handle_relay_connection_delay(cmd, message): # Downloads client side proxy to connect to Arc Connectivity Platform -def install_client_side_proxy(arc_proxy_folder): +def install_client_side_proxy(cmd, arc_proxy_folder): client_operating_system = _get_client_operating_system() client_architecture = _get_client_architeture() @@ -241,14 +241,36 @@ def install_client_side_proxy(arc_proxy_folder): for f in older_version_files: file_utils.delete_file(f, f"failed to delete older version file {f}", warning=True) - _download_proxy_from_MCR(install_dir, proxy_name, client_operating_system, client_architecture) + _download_proxy_from_MCR(cmd, install_dir, proxy_name, client_operating_system, client_architecture) _check_proxy_installation(install_dir, proxy_name) return install_location -def _download_proxy_from_MCR(dest_dir, proxy_name, operating_system, architecture): - mar_target = f"{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" +def _download_proxy_from_MCR(cmd, dest_dir, proxy_name, operating_system, architecture): + # active_directory in public cloud is login.microsoftonline.com + # the logic below dynamically creates the MCR url using a multi-part suffix for Airgapped clouds + # NST team has determined that these suffixes should be not exposed to customers + active_directory_array = cmd.cli_ctx.cloud.endpoints.active_directory.split(".") + # default for public, mc, ff clouds + mcr_postfix = active_directory_array[2] + # special cases for USSec, exclude part of suffix + if ( + len(active_directory_array) == 4 + and active_directory_array[2] == "microsoft" + ): + mcr_postfix = active_directory_array[3] + # special case for USNat + elif len(active_directory_array) == 5: + mcr_postfix = ( + active_directory_array[2] + + "." + + active_directory_array[3] + + "." + + active_directory_array[4] + ) + mcr_url = f"mcr.microsoft.{mcr_postfix}" + mar_target = f"{mcr_url}/{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" logger.debug("Downloading Arc Connectivity Proxy from %s in Microsoft Artifact Regristy.", mar_target) client = oras.client.OrasClient() @@ -400,4 +422,4 @@ def _get_client_operating_system(): if operating_system.lower() not in ('linux', 'darwin', 'windows'): raise azclierror.BadRequestError(f"Unsuported OS: {operating_system} platform is not currently supported") - return operating_system + return operating_system \ No newline at end of file diff --git a/src/ssh/azext_ssh/constants.py b/src/ssh/azext_ssh/constants.py index b75c616cf7c..7370feda9c5 100644 --- a/src/ssh/azext_ssh/constants.py +++ b/src/ssh/azext_ssh/constants.py @@ -7,8 +7,8 @@ AGENT_MINIMUM_VERSION_MAJOR = 1 AGENT_MINIMUM_VERSION_MINOR = 31 -CLIENT_PROXY_VERSION = "1.3.026973" -CLIENT_PROXY_MCR_TARGET = "mcr.microsoft.com/azureconnectivity/proxy" +CLIENT_PROXY_VERSION = "1.3.029923" +CLIENT_PROXY_MCR_TARGET = "azureconnectivity/proxy" CLEANUP_TOTAL_TIME_LIMIT_IN_SECONDS = 120 CLEANUP_TIME_INTERVAL_IN_SECONDS = 10 CLEANUP_AWAIT_TERMINATION_IN_SECONDS = 30 diff --git a/src/ssh/azext_ssh/custom.py b/src/ssh/azext_ssh/custom.py index 13baf9e3388..328c551f805 100644 --- a/src/ssh/azext_ssh/custom.py +++ b/src/ssh/azext_ssh/custom.py @@ -187,7 +187,7 @@ def _do_ssh_op(cmd, op_info, op_call): try: if op_info.is_arc(): - op_info.proxy_path = connectivity_utils.install_client_side_proxy(op_info.ssh_proxy_folder) + op_info.proxy_path = connectivity_utils.install_client_side_proxy(cmd, op_info.ssh_proxy_folder) (op_info.relay_info, op_info.new_service_config) = connectivity_utils.get_relay_information( cmd, op_info.resource_group_name, op_info.vm_name, op_info.resource_type, cert_lifetime, op_info.port, op_info.yes_without_prompt) @@ -212,9 +212,34 @@ def _get_and_write_certificate(cmd, public_key_file, cert_file, ssh_client_folde } scope = cloudtoscope.get(cmd.cli_ctx.cloud.name.lower(), None) if not scope: - raise azclierror.InvalidArgumentValueError( - f"Unsupported cloud {cmd.cli_ctx.cloud.name.lower()}", - "Supported clouds include azurecloud,azurechinacloud,azureusgovernment") + # NST team has determined Airgapped cloud endpoints should not be exposed to customers + # This dynamically creates correct scope api endpoints given generic suffixes that are 4 and 5 segments long + active_directory_graph_api_array = cmd.cli_ctx.cloud.endpoints.activeDirectoryGraphResourceId.split(".") + # special cases for USSec + if (len(active_directory_graph_api_array) == 4): + scope_postfix = ( + active_directory_graph_api_array[1] + + "." + + active_directory_graph_api_array[2] + + "." + + active_directory_graph_api_array[3] + ) + # special case for USNat + elif len(active_directory_graph_api_array) == 5: + scope_postfix = ( + active_directory_graph_api_array[1] + + "." + + active_directory_graph_api_array[2] + + "." + + active_directory_graph_api_array[3] + + "." + + active_directory_graph_api_array[4] + ) + else: + raise azclierror.InvalidArgumentValueError( + f"Unsupported cloud {cmd.cli_ctx.cloud.name.lower()}", + "Supported clouds include azurecloud,azurechinacloud,azureusgovernment") + scope = f"https://pas.{scope}/CheckMyAccess/Linux/.default" scopes = [scope] data = _prepare_jwk_data(public_key_file) @@ -363,4 +388,4 @@ def _get_modulus_exponent(public_key_file): modulus = parser.modulus exponent = parser.exponent - return modulus, exponent + return modulus, exponent \ No newline at end of file diff --git a/src/ssh/azext_ssh/tests/latest/test_connectivity_utils.py b/src/ssh/azext_ssh/tests/latest/test_connectivity_utils.py index d1d52a7d8ac..af8b73fd2ae 100644 --- a/src/ssh/azext_ssh/tests/latest/test_connectivity_utils.py +++ b/src/ssh/azext_ssh/tests/latest/test_connectivity_utils.py @@ -133,8 +133,13 @@ def test_install_proxy_create_dir(self, mock_check, mock_download, mock_dir, moc mock_get_proxy_dir.return_value = "/dir/proxy" mock_isfile.return_value = False - connectivity_utils.install_client_side_proxy(None) + cmd = mock.Mock() + cmd.cli_ctx = mock.Mock() + cmd.cli_ctx.cloud = mock.Mock() + cmd.cli_ctx.cloud.endpoints = mock.Mock() + cmd.cli_ctx.cloud.endpoints.active_directory = "https://login.microsoftonline.com" + connectivity_utils.install_client_side_proxy(cmd, None) mock_dir.assert_called_once_with("/dir/proxy", "Failed to create client proxy directory \'/dir/proxy\'.") - mock_download.assert_called_once_with("/dir/proxy", "sshProxy_linux_arm64_1_3_026973", "linux", "arm64") - mock_check.assert_called_once_with("/dir/proxy", "sshProxy_linux_arm64_1_3_026973") + mock_download.assert_called_once_with(cmd, "/dir/proxy", "sshProxy_linux_arm64_1_3_026973", "linux", "arm64") + mock_check.assert_called_once_with("/dir/proxy", "sshProxy_linux_arm64_1_3_026973") \ No newline at end of file diff --git a/src/ssh/azext_ssh/tests/latest/test_custom.py b/src/ssh/azext_ssh/tests/latest/test_custom.py index 66800a8537e..aef81b8352a 100644 --- a/src/ssh/azext_ssh/tests/latest/test_custom.py +++ b/src/ssh/azext_ssh/tests/latest/test_custom.py @@ -422,6 +422,10 @@ def test_do_ssh_op_no_public_ip(self, mock_ip, mock_check_files): def test_do_ssh_op_arc_local_user(self, mock_get_cert, mock_check_keys, mock_start_ssh, mock_get_relay_info, mock_get_proxy): mock_get_relay_info.return_value = ('relay', False) cmd = mock.Mock() + cmd.cli_ctx = mock.Mock() + cmd.cli_ctx.cloud = mock.Mock() + cmd.cli_ctx.cloud.endpoints = mock.Mock() + cmd.cli_ctx.cloud.endpoints.active_directory = "https://login.microsoftonline.com" mock_op = mock.Mock() op_info = ssh_info.SSHSession("rg", "vm", None, None, None, False, "user", None, "port", None, [], False, "Microsoft.HybridCompute/machines", None, None, False, False) @@ -432,7 +436,7 @@ def test_do_ssh_op_arc_local_user(self, mock_get_cert, mock_check_keys, mock_sta custom._do_ssh_op(cmd, op_info, mock_op) - mock_get_proxy.assert_called_once_with('proxy') + mock_get_proxy.assert_called_once_with(cmd, 'proxy') mock_get_relay_info.assert_called_once_with(cmd, 'rg', 'vm', 'Microsoft.HybridCompute/machines', None, "port", False) mock_op.assert_called_once_with(op_info, False, False) mock_get_cert.assert_not_called() @@ -457,6 +461,8 @@ def test_do_ssh_arc_op_aad_user(self, mock_cert_exp, mock_start_ssh, mock_write_ cmd.cli_ctx = mock.Mock() cmd.cli_ctx.cloud = mock.Mock() cmd.cli_ctx.cloud.name = "azurecloud" + cmd.cli_ctx.cloud.endpoints = mock.Mock() + cmd.cli_ctx.cloud.endpoints.active_directory = "https://login.microsoftonline.com" mock_check_files.return_value = "public", "private", False mock_principal.return_value = ["username"] mock_get_mod_exp.return_value = "modulus", "exponent" @@ -483,9 +489,9 @@ def test_do_ssh_arc_op_aad_user(self, mock_cert_exp, mock_start_ssh, mock_write_ mock_check_files.assert_called_once_with("publicfile", "privatefile", None, "client") mock_get_mod_exp.assert_called_once_with("public") mock_write_cert.assert_called_once_with("certificate", "public-aadcert.pub") - mock_get_proxy.assert_called_once_with('proxy') + mock_get_proxy.assert_called_once_with(cmd, 'proxy') mock_get_relay_info.assert_called_once_with(cmd, 'rg', 'vm', 'Microsoft.HybridCompute/machines', 3600, 'port', False) mock_op.assert_called_once_with(op_info, False, True) if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/src/ssh/setup.py b/src/ssh/setup.py index 4ad30289af0..9ab16e2f78d 100644 --- a/src/ssh/setup.py +++ b/src/ssh/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages -VERSION = "2.0.6" +VERSION = "2.0.7" CLASSIFIERS = [ 'Development Status :: 4 - Beta', From 7d6321cf5b55f6b3127d97870f3e7b3f5d5933c1 Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Wed, 8 Oct 2025 16:35:06 -0400 Subject: [PATCH 2/7] Fixing scope postfix issue with auth --- src/ssh/azext_ssh/connectivity_utils.py | 6 +++--- src/ssh/azext_ssh/custom.py | 28 ++++++------------------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/ssh/azext_ssh/connectivity_utils.py b/src/ssh/azext_ssh/connectivity_utils.py index 1ba9b1402b8..1f4bf550749 100644 --- a/src/ssh/azext_ssh/connectivity_utils.py +++ b/src/ssh/azext_ssh/connectivity_utils.py @@ -254,13 +254,13 @@ def _download_proxy_from_MCR(cmd, dest_dir, proxy_name, operating_system, archit active_directory_array = cmd.cli_ctx.cloud.endpoints.active_directory.split(".") # default for public, mc, ff clouds mcr_postfix = active_directory_array[2] - # special cases for USSec, exclude part of suffix + # special cases for AGC, exclude part of suffix if ( len(active_directory_array) == 4 and active_directory_array[2] == "microsoft" ): mcr_postfix = active_directory_array[3] - # special case for USNat + # special case for AGC elif len(active_directory_array) == 5: mcr_postfix = ( active_directory_array[2] @@ -270,7 +270,7 @@ def _download_proxy_from_MCR(cmd, dest_dir, proxy_name, operating_system, archit + active_directory_array[4] ) mcr_url = f"mcr.microsoft.{mcr_postfix}" - mar_target = f"{mcr_url}/{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" + mar_target = f"{mcr_url}{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" logger.debug("Downloading Arc Connectivity Proxy from %s in Microsoft Artifact Regristy.", mar_target) client = oras.client.OrasClient() diff --git a/src/ssh/azext_ssh/custom.py b/src/ssh/azext_ssh/custom.py index 328c551f805..e64577e17e1 100644 --- a/src/ssh/azext_ssh/custom.py +++ b/src/ssh/azext_ssh/custom.py @@ -215,31 +215,15 @@ def _get_and_write_certificate(cmd, public_key_file, cert_file, ssh_client_folde # NST team has determined Airgapped cloud endpoints should not be exposed to customers # This dynamically creates correct scope api endpoints given generic suffixes that are 4 and 5 segments long active_directory_graph_api_array = cmd.cli_ctx.cloud.endpoints.activeDirectoryGraphResourceId.split(".") - # special cases for USSec - if (len(active_directory_graph_api_array) == 4): - scope_postfix = ( - active_directory_graph_api_array[1] - + "." - + active_directory_graph_api_array[2] - + "." - + active_directory_graph_api_array[3] - ) - # special case for USNat - elif len(active_directory_graph_api_array) == 5: - scope_postfix = ( - active_directory_graph_api_array[1] - + "." - + active_directory_graph_api_array[2] - + "." - + active_directory_graph_api_array[3] - + "." - + active_directory_graph_api_array[4] - ) - else: + separator = "." + scope_postfix = separator.join(active_directory_graph_api_array[1:]) # default to everything after first segment + + if len(active_directory_graph_api_array) not in [4, 5]: raise azclierror.InvalidArgumentValueError( f"Unsupported cloud {cmd.cli_ctx.cloud.name.lower()}", "Supported clouds include azurecloud,azurechinacloud,azureusgovernment") - scope = f"https://pas.{scope}/CheckMyAccess/Linux/.default" + + scope = f"https://pas.{scope_postfix}/CheckMyAccess/Linux/.default" scopes = [scope] data = _prepare_jwk_data(public_key_file) From 9d77277ef4748156206e8195190ef70b8994b484 Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Fri, 10 Oct 2025 12:31:55 -0400 Subject: [PATCH 3/7] adding in style changes --- src/ssh/azext_ssh/connectivity_utils.py | 4 ++-- src/ssh/azext_ssh/custom.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ssh/azext_ssh/connectivity_utils.py b/src/ssh/azext_ssh/connectivity_utils.py index 1f4bf550749..3a6f3c73cda 100644 --- a/src/ssh/azext_ssh/connectivity_utils.py +++ b/src/ssh/azext_ssh/connectivity_utils.py @@ -270,7 +270,7 @@ def _download_proxy_from_MCR(cmd, dest_dir, proxy_name, operating_system, archit + active_directory_array[4] ) mcr_url = f"mcr.microsoft.{mcr_postfix}" - mar_target = f"{mcr_url}{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" + mar_target = f"{mcr_url}{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" logger.debug("Downloading Arc Connectivity Proxy from %s in Microsoft Artifact Regristy.", mar_target) client = oras.client.OrasClient() @@ -422,4 +422,4 @@ def _get_client_operating_system(): if operating_system.lower() not in ('linux', 'darwin', 'windows'): raise azclierror.BadRequestError(f"Unsuported OS: {operating_system} platform is not currently supported") - return operating_system \ No newline at end of file + return operating_system diff --git a/src/ssh/azext_ssh/custom.py b/src/ssh/azext_ssh/custom.py index e64577e17e1..1fda93a5c90 100644 --- a/src/ssh/azext_ssh/custom.py +++ b/src/ssh/azext_ssh/custom.py @@ -222,7 +222,7 @@ def _get_and_write_certificate(cmd, public_key_file, cert_file, ssh_client_folde raise azclierror.InvalidArgumentValueError( f"Unsupported cloud {cmd.cli_ctx.cloud.name.lower()}", "Supported clouds include azurecloud,azurechinacloud,azureusgovernment") - + scope = f"https://pas.{scope_postfix}/CheckMyAccess/Linux/.default" scopes = [scope] @@ -372,4 +372,4 @@ def _get_modulus_exponent(public_key_file): modulus = parser.modulus exponent = parser.exponent - return modulus, exponent \ No newline at end of file + return modulus, exponent From 116f2c9d0e57fe45b2af509999d90cd7e234f8ef Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Sun, 12 Oct 2025 18:56:25 -0400 Subject: [PATCH 4/7] Adding in PR feedback --- src/ssh/azext_ssh/connectivity_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ssh/azext_ssh/connectivity_utils.py b/src/ssh/azext_ssh/connectivity_utils.py index 3a6f3c73cda..4ac77ef8681 100644 --- a/src/ssh/azext_ssh/connectivity_utils.py +++ b/src/ssh/azext_ssh/connectivity_utils.py @@ -270,8 +270,10 @@ def _download_proxy_from_MCR(cmd, dest_dir, proxy_name, operating_system, archit + active_directory_array[4] ) mcr_url = f"mcr.microsoft.{mcr_postfix}" - mar_target = f"{mcr_url}{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" - logger.debug("Downloading Arc Connectivity Proxy from %s in Microsoft Artifact Regristy.", mar_target) + if mcr_url.endswith("/"): + mcr_url = mcr_url[:-1] + mar_target = f"{mcr_url}/{consts.CLIENT_PROXY_MCR_TARGET}/{operating_system.lower()}/{architecture}/ssh-proxy" + logger.debug("Downloading Arc Connectivity Proxy from %s in Microsoft Artifact Registry.", mar_target) client = oras.client.OrasClient() t0 = time.time() From 5845f4e094509a8f2383c9ab2149c468281ac5ab Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Sun, 12 Oct 2025 19:06:02 -0400 Subject: [PATCH 5/7] Updating history file --- src/ssh/HISTORY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ssh/HISTORY.md b/src/ssh/HISTORY.md index c681d5706c2..7ceb2d87c33 100644 --- a/src/ssh/HISTORY.md +++ b/src/ssh/HISTORY.md @@ -1,5 +1,8 @@ Release History =============== +2.0.7 +----- +* Add in auth handling for air gapped and sovereign environments 2.0.6 ----- * Remove msrestazure dependency From 48a04716305866eaa6308ad3cee632dd56814eef Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Tue, 14 Oct 2025 12:00:41 -0400 Subject: [PATCH 6/7] Fixing line length --- src/ssh/azext_ssh/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ssh/azext_ssh/custom.py b/src/ssh/azext_ssh/custom.py index 1fda93a5c90..7a4afdcda27 100644 --- a/src/ssh/azext_ssh/custom.py +++ b/src/ssh/azext_ssh/custom.py @@ -216,7 +216,7 @@ def _get_and_write_certificate(cmd, public_key_file, cert_file, ssh_client_folde # This dynamically creates correct scope api endpoints given generic suffixes that are 4 and 5 segments long active_directory_graph_api_array = cmd.cli_ctx.cloud.endpoints.activeDirectoryGraphResourceId.split(".") separator = "." - scope_postfix = separator.join(active_directory_graph_api_array[1:]) # default to everything after first segment + scope_postfix = separator.join(active_directory_graph_api_array[1:]) # default to all but first segment if len(active_directory_graph_api_array) not in [4, 5]: raise azclierror.InvalidArgumentValueError( From d1797726ec72c4a2ebce9c891461a4a0bd6c1ec9 Mon Sep 17 00:00:00 2001 From: Andrew Han Date: Tue, 14 Oct 2025 13:23:51 -0400 Subject: [PATCH 7/7] Updating version in setup.py --- src/ssh/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ssh/setup.py b/src/ssh/setup.py index 9ab16e2f78d..fe7d2e17745 100644 --- a/src/ssh/setup.py +++ b/src/ssh/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages -VERSION = "2.0.7" +VERSION = "2.1.0" CLASSIFIERS = [ 'Development Status :: 4 - Beta',