-
Notifications
You must be signed in to change notification settings - Fork 202
/
Copy pathcloudshell.py
122 lines (109 loc) · 4.91 KB
/
cloudshell.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# Copyright (c) Microsoft Corporation.
# All rights reserved.
#
# This code is licensed under the MIT License.
"""This module wraps Cloud Shell's IMDS-like interface inside an OAuth2-like helper"""
import base64
import json
import logging
import os
import time
try: # Python 2
from urlparse import urlparse
except: # Python 3
from urllib.parse import urlparse
from .oauth2cli.oidc import decode_part
logger = logging.getLogger(__name__)
def _is_running_in_cloud_shell():
return os.environ.get("AZUREPS_HOST_ENVIRONMENT", "").startswith("cloud-shell")
def _scope_to_resource(scope): # This is an experimental reasonable-effort approach
cloud_shell_supported_audiences = [
"https://analysis.windows.net/powerbi/api", # Came from https://msazure.visualstudio.com/One/_git/compute-CloudShell?path=/src/images/agent/env/envconfig.PROD.json
"https://pas.windows.net/CheckMyAccess/Linux/.default", # Cloud Shell accepts it as-is
]
for a in cloud_shell_supported_audiences:
if scope.startswith(a):
return a
u = urlparse(scope)
if u.scheme:
return "{}://{}".format(u.scheme, u.netloc)
return scope # There is no much else we can do here
def _obtain_token(http_client, scopes, client_id=None, data=None):
resp = http_client.post(
"http://localhost:50342/oauth2/token",
data=dict(
data or {},
resource=" ".join(map(_scope_to_resource, scopes))),
headers={"Metadata": "true"},
)
if resp.status_code >= 300:
logger.debug("Cloud Shell IMDS error: %s", resp.text)
cs_error = json.loads(resp.text).get("error", {})
return {k: v for k, v in {
"error": cs_error.get("code"),
"error_description": cs_error.get("message"),
}.items() if v}
imds_payload = json.loads(resp.text)
BEARER = "Bearer"
oauth2_response = {
"access_token": imds_payload["access_token"],
"expires_in": int(imds_payload["expires_in"]),
"token_type": imds_payload.get("token_type", BEARER),
}
expected_token_type = (data or {}).get("token_type", BEARER)
if oauth2_response["token_type"] != expected_token_type:
return { # Generate a normal error (rather than an intrusive exception)
"error": "broker_error",
"error_description": "token_type {} is not supported by this version of Azure Portal".format(
expected_token_type),
}
parts = imds_payload["access_token"].split(".")
# The following default values are useful in SSH Cert scenario
client_info = { # Default value, in case the real value will be unavailable
"uid": "user",
"utid": "cloudshell",
}
now = time.time()
preferred_username = "currentuser@cloudshell"
oauth2_response["id_token_claims"] = { # First 5 claims are required per OIDC
"iss": "cloudshell",
"sub": "user",
"aud": client_id,
"exp": now + 3600,
"iat": now,
"preferred_username": preferred_username, # Useful as MSAL account's username
}
if len(parts) == 3: # Probably a JWT. Use it to derive client_info and id token.
try:
# Data defined in https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens#payload-claims
jwt_payload = json.loads(decode_part(parts[1]))
client_info = {
# Mimic a real home_account_id,
# so that this pseudo account and a real account would interop.
"uid": jwt_payload.get("oid", "user"),
"utid": jwt_payload.get("tid", "cloudshell"),
}
oauth2_response["id_token_claims"] = {
"iss": jwt_payload["iss"],
"sub": jwt_payload["sub"], # Could use oid instead
"aud": client_id,
"exp": jwt_payload["exp"],
"iat": jwt_payload["iat"],
"preferred_username": jwt_payload.get("preferred_username") # V2
or jwt_payload.get("unique_name") # V1
or preferred_username,
}
except ValueError:
logger.debug("Unable to decode jwt payload: %s", parts[1])
oauth2_response["client_info"] = base64.b64encode(
# Mimic a client_info, so that MSAL would create an account
json.dumps(client_info).encode("utf-8")).decode("utf-8")
oauth2_response["id_token_claims"]["tid"] = client_info["utid"] # TBD
## Note: Decided to not surface resource back as scope,
## because they would cause the downstream OAuth2 code path to
## cache the token with a different scope and won't hit them later.
#if imds_payload.get("resource"):
# oauth2_response["scope"] = imds_payload["resource"]
if imds_payload.get("refresh_token"):
oauth2_response["refresh_token"] = imds_payload["refresh_token"]
return oauth2_response