Skip to content
Closed
1 change: 1 addition & 0 deletions src/connectedk8s/azext_connectedk8s/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
Error_enabling_Features = 'Error while updating agents for enabling features. Please run \"kubectl get pods -n azure-arc\" to check the pods in case of timeout error. Error: {}'
Error_disabling_Features = 'Error while updating agents for disabling features. Please run \"kubectl get pods -n azure-arc\" to check the pods in case of timeout error. Error: {}'
Proxy_Kubeconfig_During_Deletion_Fault_Type = 'Encountered proxy kubeconfig during deletion.'
AZ_CLI_ADAL_TO_MSAL_MIGRATE_VERSION = '2.30.0'
Cannot_Create_ClusterRoleBindings_Fault_Type = 'Cannot create cluster role bindings on this Kubernets cluster'
CC_Provider_Namespace_Not_Registered_Fault_Type = "Connected Cluster Provider MS.K8 namespace not registered"
Default_Namespace_Does_Not_Exist_Fault_Type = "The default namespace defined in the kubeconfig doesn't exist on the kubernetes cluster."
Expand Down
26 changes: 26 additions & 0 deletions src/connectedk8s/azext_connectedk8s/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from logging import exception
import os
import shutil
import subprocess
Expand All @@ -27,6 +28,8 @@
import azext_connectedk8s._constants as consts
from kubernetes import client as kube_client
from azure.cli.core.azclierror import CLIInternalError, ClientRequestError, ArgumentUsageError, ManualInterrupt, AzureResponseError, AzureInternalError, ValidationError
from azure.cli.core import get_default_cli
from packaging import version

logger = get_logger(__name__)

Expand Down Expand Up @@ -398,6 +401,29 @@ def names(self, names):
logger.debug("Error while trying to monkey patch the fix for list_node(): {}".format(str(ex)))


def use_msal_cache():
response_cli_version = az_cli("version --output json")
try:
cli_version = response_cli_version['azure-cli']
except Exception as ex:
raise CLIInternalError("Unable to decode the az cli version installed: {}".format(str(ex)))
if version.parse(cli_version) >= version.parse(consts.AZ_CLI_ADAL_TO_MSAL_MIGRATE_VERSION):
return True
else:
return False


def az_cli(args_str):
args = args_str.split()
cli = get_default_cli()
cli.invoke(args, out_file=open(os.devnull, 'w'))
if cli.result.result:
return cli.result.result
elif cli.result.error:
raise cli.result.error
return True


def check_provider_registrations(cli_ctx):
try:
rp_client = _resource_providers_client(cli_ctx)
Expand Down
53 changes: 40 additions & 13 deletions src/connectedk8s/azext_connectedk8s/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import stat
import platform
from azure.core.exceptions import ClientAuthenticationError
from msal_extensions.token_cache import PersistedTokenCache
import yaml
import requests
import urllib.request
Expand All @@ -25,6 +26,7 @@
from knack.prompting import prompt_y_n
from knack.prompting import NoTTYException
from azure.cli.core.commands.client_factory import get_subscription_id
from msal import PublicClientApplication, ConfidentialClientApplication
from azure.cli.core._profile import Profile
from azure.cli.core.util import sdk_no_wait
from azure.cli.core import telemetry
Expand All @@ -40,9 +42,11 @@
from azext_connectedk8s._client_factory import get_graph_client_service_principals
import azext_connectedk8s._constants as consts
import azext_connectedk8s._utils as utils
from azext_connectedk8s._utils import az_cli
from glob import glob
from .vendored_sdks.models import ConnectedCluster, ConnectedClusterIdentity, ListClusterUserCredentialProperties
from threading import Timer, Thread
import msal_extensions
import sys
import hashlib
import re
Expand Down Expand Up @@ -1635,12 +1639,16 @@ def client_side_proxy_wrapper(cmd,
requestUri = f'{consts.CSP_Storage_Url}/{consts.RELEASE_DATE_WINDOWS}/arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}.exe'
older_version_string = f'.clientproxy\\arcProxy{operating_system}*.exe'
creds_string = r'.azure\accessTokens.json'
msal_token_cache_user = r'.azure\msal_token_cache.bin'
msal_token_cache_spn = r'.azure\service_principal_entries.bin'

elif(operating_system == 'Linux' or operating_system == 'Darwin'):
install_location_string = f'.clientproxy/arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}'
requestUri = f'{consts.CSP_Storage_Url}/{consts.RELEASE_DATE_LINUX}/arcProxy{operating_system}{consts.CLIENT_PROXY_VERSION}'
older_version_string = f'.clientproxy/arcProxy{operating_system}*'
creds_string = r'.azure/accessTokens.json'
msal_token_cache_user = r'.azure/msal_token_cache.bin'
msal_token_cache_spn = r'.azure/service_principal_entries.bin'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DON'T DO THIS. THIS IS NOT SUPPORTED. See Azure/azure-cli#19853 (comment)


else:
telemetry.set_exception(exception='Unsupported OS', fault_type=consts.Unsupported_Fault_Type,
Expand Down Expand Up @@ -1738,21 +1746,40 @@ def client_side_proxy_wrapper(cmd,
raise FileOperationError("Failed to load credentials." + str(e))

user_name = account['user']['name']
use_msal_cache = utils.use_msal_cache()
if not use_msal_cache:
if user_type == 'user':
key = 'userId'
key2 = 'refreshToken'
else:
key = 'servicePrincipalId'
key2 = 'accessToken'

if user_type == 'user':
key = 'userId'
key2 = 'refreshToken'
else:
key = 'servicePrincipalId'
key2 = 'accessToken'

for i in range(len(creds_list)):
creds_obj = creds_list[i]

if key in creds_obj and creds_obj[key] == user_name:
creds = creds_obj[key2]
break
for i in range(len(creds_list)):
creds_obj = creds_list[i]

if key in creds_obj and creds_obj[key] == user_name:
creds = creds_obj[key2]
break
else:
if user_type == "user":
response_user_objectid = az_cli("ad signed-in-user show --query objectId -o tsv")
token_cache_location = os.path.expanduser(os.path.join('~', msal_token_cache_user))
persistence = msal_extensions.FilePersistenceWithDataProtection(token_cache_location)
token_cache = msal_extensions.PersistedTokenCache(persistence)
token_cache._reload_if_necessary()
home_account_id = response_user_objectid + "." + tenantId
owned_by_home_account = {"home_account_id": home_account_id}
creds_info = token_cache.find(PersistedTokenCache.CredentialType.REFRESH_TOKEN, query=owned_by_home_account)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ray Luo (@rayluo), I think this is another instance where people are hacking MSAL cache to get the refresh token.

@rayluo Ray Luo (rayluo) Dec 8, 2021

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is really unfortunate.

siriteja (@sirireddy12) , why is refresh token (RT) needed? Can the usage pattern be replaced by periodically requesting an access token, possibly via Azure CLI? (Jiashuo Li (@jiasli) , does az get-access-token ... has an equivalent helper function to be called by extensions?)

FYI: Not only the token cache helpers are considered internal, MSAL would probably NOT save RT in token cache in near future.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ray Luo (@rayluo) Refresh token is needed to retrieve PoP token. Does PoP implementation support available in MSAL python which az extension can utilize?

@rayluo Ray Luo (rayluo) Dec 9, 2021

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ray Luo (@rayluo) Refresh token is needed to retrieve PoP token. Does PoP implementation support available in MSAL python which az extension can utilize?

MSAL Python does not currently support PoP. However, I got an impression that the PoP implementation might be similar to another feature named "Ssh Cert" which MSAL Python does support, and then there is also an az extension "ssh" already handles the key management and feeds signed content to MSAL. You may take a look and see whether that pattern could be repurposed for PoP token.

(Oh, I did not notice your alias, krdhruva :-). The content above was the idea that I already mentioned in our email conversation.)

creds = creds_info[0]['secret']
else:
token_cache_location = os.path.expanduser(os.path.join('~', msal_token_cache_spn))
persistence = msal_extensions.FilePersistenceWithDataProtection(token_cache_location)
token_cache = msal_extensions.PersistedTokenCache(persistence)
token_cache._reload_if_necessary()
token_cache_string = token_cache.serialize()
cache_list = json.loads(token_cache_string)
creds = cache_list[0]['client_secret']
if creds == '':
telemetry.set_exception(exception='Credentials of user not found.', fault_type=consts.Creds_NotFound_Fault_Type,
summary='Unable to find creds of user')
Expand Down