Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Support getting SSH certificate inside Cloud Shell #22162

Merged
merged 8 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
rayluo marked this conversation as resolved.
Show resolved Hide resolved

# 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():
rayluo marked this conversation as resolved.
Show resolved Hide resolved
# 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"])
Comment on lines +25 to +34
Copy link
Member Author

@rayluo rayluo May 18, 2022

Choose a reason for hiding this comment

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

If the real central MSAL instance is somehow available here, I think we can reuse it, so that its already configured token_cache behavior will automatically be used to store SSH certs, and then this section can probably be refactored into something like below.

However, you do NOT have to make this change in this PR. We can merge this PR as-is (perhaps after MSAL 1.18 ships?) and postpone this cache improvement to a later date.

Suggested change
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"])
from .util import check_result, build_sdk_access_token
app = somehow_get_the_central_app_that_already_initialized() # TODO
result = app.acquire_token_silent_with_error(list(scopes), data=kwargs["data"])
if result is None or "error" in result:
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'
rayluo marked this conversation as resolved.
Show resolved Hide resolved
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 @ git+https://github.com/AzureAD/microsoft-authentication-library-for-python.git@cloudshell-imds', # TBD: 'msal>=1.18.0,<2.0.0',
'msrestazure~=0.6.4',
'packaging>=20.9,<22.0',
'paramiko>=2.0.8,<3.0.0',
Expand Down
3 changes: 2 additions & 1 deletion src/azure-cli/requirements.py3.Linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ jsondiff==1.3.0
knack==0.9.0
MarkupSafe==1.1.1
msal-extensions==1.0.0
msal==1.17.0
#msal==1.17.0
git+https://github.com/AzureAD/microsoft-authentication-library-for-python.git@cloudshell-imds#egg=msal
msrest==0.6.21
msrestazure==0.6.4
oauthlib==3.0.1
Expand Down