Skip to content

Commit

Permalink
[Core] Support getting SSH certificate inside Cloud Shell (Azure#22162)
Browse files Browse the repository at this point in the history
  • Loading branch information
rayluo authored May 19, 2022
1 parent 3dbcafb commit a179344
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 42 deletions.
3 changes: 3 additions & 0 deletions scripts/release/debian/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ apt-get update
# uuid-dev is used to build _uuid module: https://github.com/python/cpython/pull/3796
apt-get install -y libssl-dev libffi-dev python3-dev debhelper zlib1g-dev uuid-dev
apt-get install -y wget
# Git is not strictly necessary, but it would allow building an experimental package
# with dependency which is currently only available in its git repo feature branch.
apt-get install -y git

# Download Python source code
PYTHON_SRC_DIR=$(mktemp -d)
Expand Down
23 changes: 20 additions & 3 deletions src/azure-cli-core/azure/cli/core/auth/adal_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,29 @@ class MSIAuthenticationWrapper(MSIAuthentication):
# This method is exposed for Azure Core. Add *scopes, **kwargs to fit azure.core requirement
# pylint: disable=line-too-long
def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument
logger.debug("MSIAuthenticationWrapper.get_token invoked by Track 2 SDK with scopes=%s", scopes)
logger.debug("MSIAuthenticationWrapper.get_token: scopes=%r, kwargs=%r", scopes, kwargs)

if 'data' in kwargs:
from azure.cli.core.util import in_cloud_console
if in_cloud_console():
# Use MSAL to get VM SSH certificate
import msal
from .util import check_result, build_sdk_access_token
from .identity import AZURE_CLI_CLIENT_ID
app = msal.PublicClientApplication(
AZURE_CLI_CLIENT_ID, # Use a real client_id, so that cache would work
# TODO: This PoC does not currently maintain a token cache;
# Ideally we should reuse the real MSAL app object which has cache configured.
# token_cache=...,
)
result = app.acquire_token_interactive(list(scopes), prompt="none", data=kwargs["data"])
check_result(result, scopes=scopes)
return build_sdk_access_token(result)

from azure.cli.core.azclierror import AuthenticationError
raise AuthenticationError("VM SSH currently doesn't support managed identity or Cloud Shell.")
raise AuthenticationError("VM SSH currently doesn't support managed identity.")

# Use msrestazure to get access token
resource = scopes_to_resource(_normalize_scopes(scopes))
if resource:
# If available, use resource provided by SDK
Expand Down Expand Up @@ -55,7 +72,7 @@ def set_token(self):
import traceback
from azure.cli.core.azclierror import AzureConnectionError, AzureResponseError
try:
super(MSIAuthenticationWrapper, self).set_token()
super().set_token()
except requests.exceptions.ConnectionError as err:
logger.debug('throw requests.exceptions.ConnectionError when doing MSIAuthentication: \n%s',
traceback.format_exc())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def _get_token(self, scopes=None, **kwargs):
raise CLIError(SSLERROR_TEMPLATE.format(str(err)))

def signed_session(self, session=None):
logger.debug("CredentialAdaptor.get_token")
logger.debug("CredentialAdaptor.signed_session")
session = session or requests.Session()
token, external_tenant_tokens = self._get_token()
header = "{} {}".format('Bearer', token.token)
Expand Down
25 changes: 4 additions & 21 deletions src/azure-cli-core/azure/cli/core/auth/msal_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
"""
Credentials defined in this module are alternative implementations of credentials provided by Azure Identity.
These credentials implements azure.core.credentials.TokenCredential by exposing get_token method for Track 2
These credentials implement azure.core.credentials.TokenCredential by exposing get_token method for Track 2
SDK invocation.
"""

from knack.log import get_logger
from knack.util import CLIError
from msal import PublicClientApplication, ConfidentialClientApplication

from .util import check_result, AccessToken
from .util import check_result, build_sdk_access_token

# OAuth 2.0 client credentials flow parameter
# https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
Expand Down Expand Up @@ -87,7 +87,7 @@ def get_token(self, *scopes, **kwargs):
# launch browser, but show the error message and `az login` command instead.
else:
raise
return _build_sdk_access_token(result)
return build_sdk_access_token(result)


class ServicePrincipalCredential(ConfidentialClientApplication):
Expand Down Expand Up @@ -130,21 +130,4 @@ def get_token(self, *scopes, **kwargs):
if not result:
result = self.acquire_token_for_client(scopes, **kwargs)
check_result(result)
return _build_sdk_access_token(result)


def _build_sdk_access_token(token_entry):
import time
request_time = int(time.time())

# MSAL token entry sample:
# {
# 'access_token': 'eyJ0eXAiOiJKV...',
# 'token_type': 'Bearer',
# 'expires_in': 1618
# }

# Importing azure.core.credentials.AccessToken is expensive.
# This can slow down commands that doesn't need azure.core, like `az account get-access-token`.
# So We define our own AccessToken.
return AccessToken(token_entry["access_token"], request_time + token_entry["expires_in"])
return build_sdk_access_token(result)
43 changes: 30 additions & 13 deletions src/azure-cli-core/azure/cli/core/auth/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,27 @@ def aad_error_handler(error, **kwargs):
# https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes
# Search for an error code at https://login.microsoftonline.com/error

# To trigger this function for testing, simply provide an invalid scope:
# az account get-access-token --scope https://my-invalid-scope

from azure.cli.core.util import in_cloud_console
if in_cloud_console():
import socket
logger.warning("A Cloud Shell credential problem occurred. When you report the issue with the error "
"below, please mention the hostname '%s'", socket.gethostname())

msg = error.get('error_description')
login_message = _generate_login_message(**kwargs)
error_description = error.get('error_description')

# Build recommendation message
login_command = _generate_login_command(**kwargs)
login_message = (
# Cloud Shell uses IMDS-like interface for implicit login. If getting token/cert failed,
# we let the user explicitly log in to AAD with MSAL.
"Please explicitly log in with:\n{}" if error.get('error') == 'broker_error'
else "To re-authenticate, please run:\n{}").format(login_command)

from azure.cli.core.azclierror import AuthenticationError
raise AuthenticationError(msg, recommendation=login_message)
raise AuthenticationError(error_description, recommendation=login_message)


def _generate_login_command(scopes=None):
Expand All @@ -43,16 +53,6 @@ def _generate_login_command(scopes=None):
return ' '.join(login_command)


def _generate_login_message(**kwargs):
from azure.cli.core.util import in_cloud_console
login_command = _generate_login_command(**kwargs)

msg = "To re-authenticate, please {}" .format(
"refresh Azure Portal." if in_cloud_console() else "run:\n{}".format(login_command))

return msg


def resource_to_scopes(resource):
"""Convert the ADAL resource ID to MSAL scopes by appending the /.default suffix and return a list.
For example:
Expand Down Expand Up @@ -139,6 +139,23 @@ def check_result(result, **kwargs):
return None


def build_sdk_access_token(token_entry):
import time
request_time = int(time.time())

# MSAL token entry sample:
# {
# 'access_token': 'eyJ0eXAiOiJKV...',
# 'token_type': 'Bearer',
# 'expires_in': 1618
# }

# Importing azure.core.credentials.AccessToken is expensive.
# This can slow down commands that doesn't need azure.core, like `az account get-access-token`.
# So We define our own AccessToken.
return AccessToken(token_entry["access_token"], request_time + token_entry["expires_in"])


def decode_access_token(access_token):
# Decode the access token. We can do the same with https://jwt.ms
from msal.oauth2cli.oidc import decode_part
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli-core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
'jmespath',
'knack~=0.9.0',
'msal-extensions~=1.0.0',
'msal>=1.17.0,<2.0.0',
'msal==1.18.0b1',
'msrestazure~=0.6.4',
'packaging>=20.9,<22.0',
'paramiko>=2.0.8,<3.0.0',
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.Darwin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jsondiff==2.0.0
knack==0.9.0
MarkupSafe==1.1.1
msal-extensions==1.0.0
msal==1.17.0
msal==1.18.0b1
msrest==0.6.21
msrestazure==0.6.4
oauthlib==3.0.1
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.Linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jsondiff==2.0.0
knack==0.9.0
MarkupSafe==1.1.1
msal-extensions==1.0.0
msal==1.17.0
msal==1.18.0b1
msrest==0.6.21
msrestazure==0.6.4
oauthlib==3.0.1
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jsondiff==2.0.0
knack==0.9.0
MarkupSafe==1.1.1
msal-extensions==1.0.0
msal==1.17.0
msal==1.18.0b1
msrest==0.6.21
msrestazure==0.6.4
oauthlib==3.0.1
Expand Down

0 comments on commit a179344

Please sign in to comment.