py2docfx 0.1.19rc2173760__py3-none-any.whl → 0.1.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py2docfx/__main__.py +2 -2
- py2docfx/convert_prepare/arg_parser.py +1 -1
- py2docfx/convert_prepare/get_source.py +40 -8
- py2docfx/convert_prepare/git.py +1 -1
- py2docfx/docfx_yaml/logger.py +23 -19
- py2docfx/venv/basevenv/Lib/site-packages/certifi/__init__.py +1 -1
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/authorization_code.py +1 -1
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/broker.py +79 -0
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/chained.py +8 -2
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/default.py +145 -54
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/imds.py +25 -1
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/vscode.py +163 -144
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_credentials/workload_identity.py +23 -12
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/__init__.py +2 -0
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/pipeline.py +4 -2
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/utils.py +85 -0
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_version.py +1 -1
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/authorization_code.py +1 -1
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/default.py +112 -56
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/imds.py +27 -1
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/on_behalf_of.py +1 -1
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/vscode.py +15 -67
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/aio/_credentials/workload_identity.py +17 -13
- py2docfx/venv/venv1/Lib/site-packages/certifi/__init__.py +1 -1
- py2docfx/venv/venv1/Lib/site-packages/cryptography/__about__.py +1 -1
- py2docfx/venv/venv1/Lib/site-packages/msal/__main__.py +2 -0
- py2docfx/venv/venv1/Lib/site-packages/msal/application.py +63 -21
- py2docfx/venv/venv1/Lib/site-packages/msal/broker.py +5 -2
- py2docfx/venv/venv1/Lib/site-packages/msal/exceptions.py +19 -5
- py2docfx/venv/venv1/Lib/site-packages/msal/managed_identity.py +50 -10
- py2docfx/venv/venv1/Lib/site-packages/msal/sku.py +1 -1
- py2docfx/venv/venv1/Lib/site-packages/msal/throttled_http_client.py +4 -1
- {py2docfx-0.1.19rc2173760.dist-info → py2docfx-0.1.20.dist-info}/METADATA +1 -1
- {py2docfx-0.1.19rc2173760.dist-info → py2docfx-0.1.20.dist-info}/RECORD +36 -38
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/linux_vscode_adapter.py +0 -100
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/macos_vscode_adapter.py +0 -34
- py2docfx/venv/venv1/Lib/site-packages/azure/identity/_internal/win_vscode_adapter.py +0 -77
- {py2docfx-0.1.19rc2173760.dist-info → py2docfx-0.1.20.dist-info}/WHEEL +0 -0
- {py2docfx-0.1.19rc2173760.dist-info → py2docfx-0.1.20.dist-info}/top_level.txt +0 -0
py2docfx/__main__.py
CHANGED
@@ -106,8 +106,8 @@ async def main(argv) -> int:
|
|
106
106
|
|
107
107
|
# create log folder and package log folder
|
108
108
|
log_folder = os.path.join(PACKAGE_ROOT, LOG_FOLDER)
|
109
|
-
os.makedirs(log_folder)
|
110
|
-
os.makedirs(os.path.join(log_folder, "package_logs"))
|
109
|
+
os.makedirs(log_folder, exist_ok=True)
|
110
|
+
os.makedirs(os.path.join(log_folder, "package_logs"), exist_ok=True)
|
111
111
|
|
112
112
|
py2docfxLogger.decide_global_log_level(verbose, show_warning)
|
113
113
|
|
@@ -162,7 +162,7 @@ def parse_command_line_args(argv) -> (
|
|
162
162
|
ado_token, output_root, verbose, show_warning)
|
163
163
|
elif args.param_json:
|
164
164
|
(package_info_list, required_packages) = load_command_params(args.param_json)
|
165
|
-
return (package_info_list, required_packages, github_token,
|
165
|
+
return (list(package_info_list), list(required_packages), github_token,
|
166
166
|
ado_token, output_root, verbose, show_warning)
|
167
167
|
else:
|
168
168
|
package_info = PackageInfo()
|
@@ -41,10 +41,11 @@ def update_package_info(executable: str, pkg: PackageInfo, source_folder: str):
|
|
41
41
|
|
42
42
|
setattr(pkg, attr, attr_val)
|
43
43
|
else:
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
if
|
44
|
+
# Find .dist-info folders
|
45
|
+
dist_info_folders = [f for f in all_files if path.isdir(f) and f.endswith(".dist-info")]
|
46
|
+
|
47
|
+
if dist_info_folders:
|
48
|
+
folder = dist_info_folders[0] # Take the first one if multiple exist
|
48
49
|
if path.exists(path.join(folder, "METADATA")):
|
49
50
|
with open(
|
50
51
|
path.join(folder, "METADATA"), "r", encoding="utf-8"
|
@@ -97,6 +98,9 @@ async def get_source(executable: str, pkg: PackageInfo, cnt: int, vststoken=None
|
|
97
98
|
sys.path.insert(0, source_folder)
|
98
99
|
elif pkg.install_type == PackageInfo.InstallType.PYPI:
|
99
100
|
full_name = pkg.get_combined_name_version()
|
101
|
+
# Ensure the dist directory exists
|
102
|
+
os.makedirs(dist_dir, exist_ok=True)
|
103
|
+
|
100
104
|
await pip_utils.download(
|
101
105
|
full_name,
|
102
106
|
dist_dir,
|
@@ -104,25 +108,53 @@ async def get_source(executable: str, pkg: PackageInfo, cnt: int, vststoken=None
|
|
104
108
|
prefer_source_distribution=pkg.prefer_source_distribution,
|
105
109
|
)
|
106
110
|
# unpack the downloaded wheel file.
|
107
|
-
|
111
|
+
dist_files = os.listdir(dist_dir)
|
112
|
+
if not dist_files:
|
113
|
+
msg = f"No files downloaded to {dist_dir} for package {pkg.name}"
|
114
|
+
py2docfx_logger.error(msg)
|
115
|
+
raise FileNotFoundError(f"No files found in {dist_dir}")
|
116
|
+
|
117
|
+
downloaded_dist_file = path.join(dist_dir, dist_files[0])
|
108
118
|
await pack.unpack_dist(pkg.name, downloaded_dist_file)
|
109
119
|
os.remove(downloaded_dist_file)
|
120
|
+
dist_files = os.listdir(dist_dir)
|
121
|
+
if not dist_files:
|
122
|
+
msg = f"No files found in {dist_dir} after unpacking for package {pkg.name}"
|
123
|
+
py2docfx_logger.error(msg)
|
124
|
+
raise FileNotFoundError(f"No files found in {dist_dir} after unpacking")
|
125
|
+
|
110
126
|
source_folder = path.join(
|
111
127
|
path.dirname(downloaded_dist_file),
|
112
|
-
|
128
|
+
dist_files[0]
|
113
129
|
)
|
114
130
|
elif pkg.install_type == PackageInfo.InstallType.DIST_FILE:
|
131
|
+
# Ensure the dist directory exists
|
132
|
+
os.makedirs(dist_dir, exist_ok=True)
|
133
|
+
|
115
134
|
await pip_utils.download(pkg.location, dist_dir, prefer_source_distribution=False)
|
116
135
|
# unpack the downloaded dist file.
|
117
|
-
|
136
|
+
dist_files = os.listdir(dist_dir)
|
137
|
+
if not dist_files:
|
138
|
+
msg = f"No files downloaded to {dist_dir} for package {pkg.name}"
|
139
|
+
py2docfx_logger.error(msg)
|
140
|
+
raise FileNotFoundError(f"No files found in {dist_dir}")
|
141
|
+
|
142
|
+
downloaded_dist_file = path.join(dist_dir, dist_files[0])
|
118
143
|
await pack.unpack_dist(pkg.name, downloaded_dist_file)
|
119
144
|
os.remove(downloaded_dist_file)
|
145
|
+
|
146
|
+
# Check again after unpacking
|
147
|
+
dist_files = os.listdir(dist_dir)
|
148
|
+
if not dist_files:
|
149
|
+
msg = f"No files found in {dist_dir} after unpacking for package {pkg.name}"
|
150
|
+
py2docfx_logger.error(msg)
|
151
|
+
raise FileNotFoundError(f"No files found in {dist_dir} after unpacking")
|
120
152
|
if downloaded_dist_file.endswith(".tar.gz"):
|
121
153
|
downloaded_dist_file = downloaded_dist_file.rsplit(".", maxsplit=1)[
|
122
154
|
0]
|
123
155
|
source_folder = path.join(
|
124
156
|
path.dirname(downloaded_dist_file),
|
125
|
-
|
157
|
+
dist_files[0] if dist_files else ""
|
126
158
|
)
|
127
159
|
else:
|
128
160
|
msg = f"Unknown install type: {pkg.install_type}"
|
py2docfx/convert_prepare/git.py
CHANGED
@@ -25,7 +25,7 @@ async def clone(repo_location, branch, folder, extra_token=None):
|
|
25
25
|
raise ValueError(msg)
|
26
26
|
else:
|
27
27
|
# Remove http(s):// from url to record. Further avoid dup-clone.
|
28
|
-
pureURL = re.sub("^\s*https?://", "", repo_location)
|
28
|
+
pureURL = re.sub(r"^\s*https?://", "", repo_location)
|
29
29
|
|
30
30
|
if pureURL not in repoMap:
|
31
31
|
branch = convertBranch(repo_location, branch, extra_token)
|
py2docfx/docfx_yaml/logger.py
CHANGED
@@ -77,16 +77,18 @@ def counts_errors_warnings(log_file_path):
|
|
77
77
|
warning_count += 1
|
78
78
|
return warning_count, error_count
|
79
79
|
|
80
|
-
def get_warning_error_count():
|
81
|
-
main_log_file_path = os.path.join("logs", "log.txt")
|
82
|
-
warning_count, error_count = counts_errors_warnings(main_log_file_path)
|
83
|
-
|
84
|
-
log_folder_path = os.path.join("logs", "package_logs")
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
80
|
+
def get_warning_error_count():
|
81
|
+
main_log_file_path = os.path.join("logs", "log.txt")
|
82
|
+
warning_count, error_count = counts_errors_warnings(main_log_file_path)
|
83
|
+
|
84
|
+
log_folder_path = os.path.join("logs", "package_logs")
|
85
|
+
# Check if the directory exists before trying to list its contents
|
86
|
+
if os.path.exists(log_folder_path) and os.path.isdir(log_folder_path):
|
87
|
+
for log_file in os.listdir(log_folder_path):
|
88
|
+
log_file_path = os.path.join(log_folder_path, log_file)
|
89
|
+
warnings, errors = counts_errors_warnings(log_file_path)
|
90
|
+
warning_count += warnings
|
91
|
+
error_count += errors
|
90
92
|
|
91
93
|
return warning_count, error_count
|
92
94
|
|
@@ -118,15 +120,17 @@ def print_out_log_by_log_level(log_list, log_level):
|
|
118
120
|
if log['level'] >= log_level and log['message'] not in ['', '\n', '\r\n']:
|
119
121
|
print(log['message'])
|
120
122
|
|
121
|
-
def output_log_by_log_level():
|
122
|
-
log_level = get_log_level()
|
123
|
-
main_log_file_path = os.path.join("logs", "log.txt")
|
124
|
-
print_out_log_by_log_level(parse_log(main_log_file_path), log_level)
|
125
|
-
|
126
|
-
package_logs_folder = os.path.join("logs", "package_logs")
|
127
|
-
|
128
|
-
|
129
|
-
|
123
|
+
def output_log_by_log_level():
|
124
|
+
log_level = get_log_level()
|
125
|
+
main_log_file_path = os.path.join("logs", "log.txt")
|
126
|
+
print_out_log_by_log_level(parse_log(main_log_file_path), log_level)
|
127
|
+
|
128
|
+
package_logs_folder = os.path.join("logs", "package_logs")
|
129
|
+
# Check if the directory exists before trying to list its contents
|
130
|
+
if os.path.exists(package_logs_folder) and os.path.isdir(package_logs_folder):
|
131
|
+
for log_file in os.listdir(package_logs_folder):
|
132
|
+
log_file_path = os.path.join(package_logs_folder, log_file)
|
133
|
+
print_out_log_by_log_level(parse_log(log_file_path), log_level)
|
130
134
|
|
131
135
|
async def run_async_subprocess(exe_path, cmd, logger, cwd=None):
|
132
136
|
if cwd is None:
|
@@ -127,7 +127,7 @@ class AuthorizationCodeCredential(GetTokenMixin):
|
|
127
127
|
return token
|
128
128
|
|
129
129
|
token = None
|
130
|
-
for refresh_token in self._client.get_cached_refresh_tokens(scopes):
|
130
|
+
for refresh_token in self._client.get_cached_refresh_tokens(scopes, **kwargs):
|
131
131
|
if "secret" in refresh_token:
|
132
132
|
token = self._client.obtain_token_by_refresh_token(scopes, refresh_token["secret"], **kwargs)
|
133
133
|
if token:
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# ------------------------------------
|
2
|
+
# Copyright (c) Microsoft Corporation.
|
3
|
+
# Licensed under the MIT License.
|
4
|
+
# ------------------------------------
|
5
|
+
import sys
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
import msal
|
9
|
+
from azure.core.credentials import AccessToken, AccessTokenInfo, SupportsTokenInfo
|
10
|
+
from .._exceptions import CredentialUnavailableError
|
11
|
+
from .._internal.utils import get_broker_credential, is_wsl
|
12
|
+
|
13
|
+
|
14
|
+
class BrokerCredential(SupportsTokenInfo):
|
15
|
+
"""A broker credential that handles prerequisite checking and falls back appropriately.
|
16
|
+
|
17
|
+
This credential checks if the azure-identity-broker package is available and the platform
|
18
|
+
is supported. If both conditions are met, it uses the real broker credential. Otherwise,
|
19
|
+
it raises CredentialUnavailableError with an appropriate message.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, **kwargs: Any) -> None:
|
23
|
+
|
24
|
+
self._tenant_id = kwargs.pop("tenant_id", None)
|
25
|
+
self._client_id = kwargs.pop("client_id", None)
|
26
|
+
self._broker_credential = None
|
27
|
+
self._unavailable_message = None
|
28
|
+
|
29
|
+
# Check prerequisites and initialize the appropriate credential
|
30
|
+
broker_credential_class = get_broker_credential()
|
31
|
+
if broker_credential_class and (sys.platform.startswith("win") or is_wsl()):
|
32
|
+
# The silent auth flow for brokered auth is available on Windows/WSL with the broker package
|
33
|
+
try:
|
34
|
+
broker_credential_args = {
|
35
|
+
"tenant_id": self._tenant_id,
|
36
|
+
"parent_window_handle": msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE,
|
37
|
+
"use_default_broker_account": True,
|
38
|
+
"disable_interactive_fallback": True,
|
39
|
+
**kwargs,
|
40
|
+
}
|
41
|
+
if self._client_id:
|
42
|
+
broker_credential_args["client_id"] = self._client_id
|
43
|
+
self._broker_credential = broker_credential_class(**broker_credential_args)
|
44
|
+
except Exception as ex: # pylint: disable=broad-except
|
45
|
+
self._unavailable_message = f"InteractiveBrowserBrokerCredential initialization failed: {ex}"
|
46
|
+
else:
|
47
|
+
# Determine the specific reason for unavailability
|
48
|
+
if broker_credential_class is None:
|
49
|
+
self._unavailable_message = (
|
50
|
+
"InteractiveBrowserBrokerCredential unavailable. "
|
51
|
+
"The 'azure-identity-broker' package is required to use brokered authentication."
|
52
|
+
)
|
53
|
+
else:
|
54
|
+
self._unavailable_message = (
|
55
|
+
"InteractiveBrowserBrokerCredential unavailable. "
|
56
|
+
"Brokered authentication is only supported on Windows and WSL platforms."
|
57
|
+
)
|
58
|
+
|
59
|
+
def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
|
60
|
+
if self._broker_credential:
|
61
|
+
return self._broker_credential.get_token(*scopes, **kwargs)
|
62
|
+
raise CredentialUnavailableError(message=self._unavailable_message)
|
63
|
+
|
64
|
+
def get_token_info(self, *scopes: str, **kwargs: Any) -> AccessTokenInfo:
|
65
|
+
if self._broker_credential:
|
66
|
+
return self._broker_credential.get_token_info(*scopes, **kwargs)
|
67
|
+
raise CredentialUnavailableError(message=self._unavailable_message)
|
68
|
+
|
69
|
+
def __enter__(self) -> "BrokerCredential":
|
70
|
+
if self._broker_credential:
|
71
|
+
self._broker_credential.__enter__()
|
72
|
+
return self
|
73
|
+
|
74
|
+
def __exit__(self, *args):
|
75
|
+
if self._broker_credential:
|
76
|
+
self._broker_credential.__exit__(*args)
|
77
|
+
|
78
|
+
def close(self) -> None:
|
79
|
+
self.__exit__()
|
@@ -23,10 +23,16 @@ _LOGGER = logging.getLogger(__name__)
|
|
23
23
|
def _get_error_message(history):
|
24
24
|
attempts = []
|
25
25
|
for credential, error in history:
|
26
|
+
# Check if credential has a custom name (for DACErrorReporter instances)
|
27
|
+
if hasattr(credential, "_credential_name"):
|
28
|
+
credential_name = credential._credential_name # pylint: disable=protected-access
|
29
|
+
else:
|
30
|
+
credential_name = credential.__class__.__name__
|
31
|
+
|
26
32
|
if error:
|
27
|
-
attempts.append("{}: {}".format(
|
33
|
+
attempts.append("{}: {}".format(credential_name, error))
|
28
34
|
else:
|
29
|
-
attempts.append(
|
35
|
+
attempts.append(credential_name)
|
30
36
|
return """
|
31
37
|
Attempted credentials:\n\t{}""".format(
|
32
38
|
"\n\t".join(attempts)
|
@@ -6,10 +6,18 @@ import logging
|
|
6
6
|
import os
|
7
7
|
from typing import List, Any, Optional, cast
|
8
8
|
|
9
|
-
from azure.core.credentials import
|
9
|
+
from azure.core.credentials import (
|
10
|
+
AccessToken,
|
11
|
+
AccessTokenInfo,
|
12
|
+
TokenRequestOptions,
|
13
|
+
SupportsTokenInfo,
|
14
|
+
TokenCredential,
|
15
|
+
)
|
16
|
+
from .. import CredentialUnavailableError
|
10
17
|
from .._constants import EnvironmentVariables
|
11
|
-
from .._internal import get_default_authority, normalize_authority, within_dac
|
18
|
+
from .._internal.utils import get_default_authority, normalize_authority, within_dac, process_credential_exclusions
|
12
19
|
from .azure_powershell import AzurePowerShellCredential
|
20
|
+
from .broker import BrokerCredential
|
13
21
|
from .browser import InteractiveBrowserCredential
|
14
22
|
from .chained import ChainedTokenCredential
|
15
23
|
from .environment import EnvironmentCredential
|
@@ -23,6 +31,32 @@ from .workload_identity import WorkloadIdentityCredential
|
|
23
31
|
_LOGGER = logging.getLogger(__name__)
|
24
32
|
|
25
33
|
|
34
|
+
class FailedDACCredential:
|
35
|
+
"""This acts as a substitute for a credential that has failed to initialize in the DAC chain.
|
36
|
+
|
37
|
+
This allows instantiation errors to be reported in ChainTokenCredential if all token requests fail.
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self, credential_name: str, error: str) -> None:
|
41
|
+
self._error = error
|
42
|
+
self._credential_name = credential_name
|
43
|
+
|
44
|
+
def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
|
45
|
+
raise CredentialUnavailableError(self._error)
|
46
|
+
|
47
|
+
def get_token_info(self, *scopes, options: Optional[TokenRequestOptions] = None, **kwargs: Any) -> AccessTokenInfo:
|
48
|
+
raise CredentialUnavailableError(self._error)
|
49
|
+
|
50
|
+
def __enter__(self) -> "FailedDACCredential":
|
51
|
+
return self
|
52
|
+
|
53
|
+
def __exit__(self, *args: Any) -> None:
|
54
|
+
pass
|
55
|
+
|
56
|
+
def close(self) -> None:
|
57
|
+
pass
|
58
|
+
|
59
|
+
|
26
60
|
class DefaultAzureCredential(ChainedTokenCredential):
|
27
61
|
"""A credential capable of handling most Azure SDK authentication scenarios. For more information, See
|
28
62
|
`Usage guidance for DefaultAzureCredential
|
@@ -42,6 +76,8 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
42
76
|
5. The identity currently logged in to the Azure CLI.
|
43
77
|
6. The identity currently logged in to Azure PowerShell.
|
44
78
|
7. The identity currently logged in to the Azure Developer CLI.
|
79
|
+
8. Brokered authentication. On Windows and WSL only, this uses the default account logged in via
|
80
|
+
Web Account Manager (WAM) if the `azure-identity-broker` package is installed.
|
45
81
|
|
46
82
|
This default behavior is configurable with keyword arguments.
|
47
83
|
|
@@ -64,9 +100,13 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
64
100
|
**False**.
|
65
101
|
:keyword bool exclude_interactive_browser_credential: Whether to exclude interactive browser authentication (see
|
66
102
|
:class:`~azure.identity.InteractiveBrowserCredential`). Defaults to **True**.
|
103
|
+
:keyword bool exclude_broker_credential: Whether to exclude the broker credential from the credential chain.
|
104
|
+
Defaults to **False**.
|
67
105
|
:keyword str interactive_browser_tenant_id: Tenant ID to use when authenticating a user through
|
68
106
|
:class:`~azure.identity.InteractiveBrowserCredential`. Defaults to the value of environment variable
|
69
107
|
AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants.
|
108
|
+
:keyword str broker_tenant_id: The tenant ID to use when using brokered authentication. Defaults to the value of
|
109
|
+
environment variable AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants.
|
70
110
|
:keyword str managed_identity_client_id: The client ID of a user-assigned managed identity. Defaults to the value
|
71
111
|
of the environment variable AZURE_CLIENT_ID, if any. If not specified, a system-assigned identity will be used.
|
72
112
|
:keyword str workload_identity_client_id: The client ID of an identity assigned to the pod. Defaults to the value
|
@@ -75,14 +115,15 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
75
115
|
Defaults to the value of environment variable AZURE_TENANT_ID, if any.
|
76
116
|
:keyword str interactive_browser_client_id: The client ID to be used in interactive browser credential. If not
|
77
117
|
specified, users will authenticate to an Azure development application.
|
118
|
+
:keyword str broker_client_id: The client ID to be used in brokered authentication. If not specified, users will
|
119
|
+
authenticate to an Azure development application.
|
78
120
|
:keyword str shared_cache_username: Preferred username for :class:`~azure.identity.SharedTokenCacheCredential`.
|
79
121
|
Defaults to the value of environment variable AZURE_USERNAME, if any.
|
80
122
|
:keyword str shared_cache_tenant_id: Preferred tenant for :class:`~azure.identity.SharedTokenCacheCredential`.
|
81
123
|
Defaults to the value of environment variable AZURE_TENANT_ID, if any.
|
82
124
|
:keyword str visual_studio_code_tenant_id: Tenant ID to use when authenticating with
|
83
|
-
:class:`~azure.identity.VisualStudioCodeCredential`. Defaults to the
|
84
|
-
|
85
|
-
Directory work or school accounts.
|
125
|
+
:class:`~azure.identity.VisualStudioCodeCredential`. Defaults to the tenant specified in the authentication
|
126
|
+
record file used by the Azure Resources extension.
|
86
127
|
:keyword int process_timeout: The timeout in seconds to use for developer credentials that run
|
87
128
|
subprocesses (e.g. AzureCliCredential, AzurePowerShellCredential). Defaults to **10** seconds.
|
88
129
|
|
@@ -101,18 +142,10 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
101
142
|
raise TypeError("'tenant_id' is not supported in DefaultAzureCredential.")
|
102
143
|
|
103
144
|
authority = kwargs.pop("authority", None)
|
104
|
-
|
105
|
-
vscode_tenant_id = kwargs.pop(
|
106
|
-
"visual_studio_code_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID)
|
107
|
-
)
|
108
|
-
vscode_args = dict(kwargs)
|
109
|
-
if authority:
|
110
|
-
vscode_args["authority"] = authority
|
111
|
-
if vscode_tenant_id:
|
112
|
-
vscode_args["tenant_id"] = vscode_tenant_id
|
113
|
-
|
114
145
|
authority = normalize_authority(authority) if authority else get_default_authority()
|
115
146
|
|
147
|
+
vscode_tenant_id = kwargs.pop("visual_studio_code_tenant_id", None)
|
148
|
+
|
116
149
|
interactive_browser_tenant_id = kwargs.pop(
|
117
150
|
"interactive_browser_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID)
|
118
151
|
)
|
@@ -126,6 +159,9 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
126
159
|
)
|
127
160
|
interactive_browser_client_id = kwargs.pop("interactive_browser_client_id", None)
|
128
161
|
|
162
|
+
broker_tenant_id = kwargs.pop("broker_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID))
|
163
|
+
broker_client_id = kwargs.pop("broker_client_id", None)
|
164
|
+
|
129
165
|
shared_cache_username = kwargs.pop("shared_cache_username", os.environ.get(EnvironmentVariables.AZURE_USERNAME))
|
130
166
|
shared_cache_tenant_id = kwargs.pop(
|
131
167
|
"shared_cache_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID)
|
@@ -133,52 +169,97 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
133
169
|
|
134
170
|
process_timeout = kwargs.pop("process_timeout", 10)
|
135
171
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
"
|
165
|
-
|
172
|
+
# Define credential configuration mapping
|
173
|
+
credential_config = {
|
174
|
+
"environment": {
|
175
|
+
"exclude_param": "exclude_environment_credential",
|
176
|
+
"env_name": "environmentcredential",
|
177
|
+
"default_exclude": False,
|
178
|
+
},
|
179
|
+
"workload_identity": {
|
180
|
+
"exclude_param": "exclude_workload_identity_credential",
|
181
|
+
"env_name": "workloadidentitycredential",
|
182
|
+
"default_exclude": False,
|
183
|
+
},
|
184
|
+
"managed_identity": {
|
185
|
+
"exclude_param": "exclude_managed_identity_credential",
|
186
|
+
"env_name": "managedidentitycredential",
|
187
|
+
"default_exclude": False,
|
188
|
+
},
|
189
|
+
"shared_token_cache": {
|
190
|
+
"exclude_param": "exclude_shared_token_cache_credential",
|
191
|
+
"default_exclude": False,
|
192
|
+
},
|
193
|
+
"visual_studio_code": {
|
194
|
+
"exclude_param": "exclude_visual_studio_code_credential",
|
195
|
+
"env_name": "visualstudiocodecredential",
|
196
|
+
"default_exclude": False,
|
197
|
+
},
|
198
|
+
"cli": {
|
199
|
+
"exclude_param": "exclude_cli_credential",
|
200
|
+
"env_name": "azureclicredential",
|
201
|
+
"default_exclude": False,
|
202
|
+
},
|
203
|
+
"developer_cli": {
|
204
|
+
"exclude_param": "exclude_developer_cli_credential",
|
205
|
+
"env_name": "azuredeveloperclicredential",
|
206
|
+
"default_exclude": False,
|
207
|
+
},
|
208
|
+
"powershell": {
|
209
|
+
"exclude_param": "exclude_powershell_credential",
|
210
|
+
"env_name": "azurepowershellcredential",
|
211
|
+
"default_exclude": False,
|
212
|
+
},
|
213
|
+
"interactive_browser": {
|
214
|
+
"exclude_param": "exclude_interactive_browser_credential",
|
215
|
+
"env_name": "interactivebrowsercredential",
|
216
|
+
"default_exclude": True,
|
217
|
+
},
|
218
|
+
"broker": {
|
219
|
+
"exclude_param": "exclude_broker_credential",
|
220
|
+
"default_exclude": False,
|
221
|
+
},
|
222
|
+
}
|
223
|
+
|
224
|
+
# Extract user-provided exclude flags and set defaults
|
225
|
+
exclude_flags = {}
|
226
|
+
user_excludes = {}
|
227
|
+
for cred_key, config in credential_config.items():
|
228
|
+
param_name = cast(str, config["exclude_param"])
|
229
|
+
user_excludes[cred_key] = kwargs.pop(param_name, None)
|
230
|
+
exclude_flags[cred_key] = config["default_exclude"]
|
231
|
+
|
232
|
+
# Process AZURE_TOKEN_CREDENTIALS environment variable and apply user overrides
|
233
|
+
exclude_flags = process_credential_exclusions(credential_config, exclude_flags, user_excludes)
|
234
|
+
|
235
|
+
# Extract individual exclude flags for backward compatibility
|
236
|
+
exclude_environment_credential = exclude_flags["environment"]
|
237
|
+
exclude_workload_identity_credential = exclude_flags["workload_identity"]
|
238
|
+
exclude_managed_identity_credential = exclude_flags["managed_identity"]
|
239
|
+
exclude_shared_token_cache_credential = exclude_flags["shared_token_cache"]
|
240
|
+
exclude_visual_studio_code_credential = exclude_flags["visual_studio_code"]
|
241
|
+
exclude_cli_credential = exclude_flags["cli"]
|
242
|
+
exclude_developer_cli_credential = exclude_flags["developer_cli"]
|
243
|
+
exclude_powershell_credential = exclude_flags["powershell"]
|
244
|
+
exclude_interactive_browser_credential = exclude_flags["interactive_browser"]
|
245
|
+
exclude_broker_credential = exclude_flags["broker"]
|
166
246
|
|
167
247
|
credentials: List[SupportsTokenInfo] = []
|
168
248
|
within_dac.set(True)
|
169
249
|
if not exclude_environment_credential:
|
170
250
|
credentials.append(EnvironmentCredential(authority=authority, _within_dac=True, **kwargs))
|
171
251
|
if not exclude_workload_identity_credential:
|
172
|
-
|
173
|
-
client_id = workload_identity_client_id
|
252
|
+
try:
|
174
253
|
credentials.append(
|
175
254
|
WorkloadIdentityCredential(
|
176
|
-
client_id=cast(str,
|
255
|
+
client_id=cast(str, workload_identity_client_id),
|
177
256
|
tenant_id=workload_identity_tenant_id,
|
178
|
-
token_file_path=os.environ
|
257
|
+
token_file_path=os.environ.get(EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE),
|
179
258
|
**kwargs,
|
180
259
|
)
|
181
260
|
)
|
261
|
+
except ValueError as ex:
|
262
|
+
credentials.append(FailedDACCredential("WorkloadIdentityCredential", error=str(ex)))
|
182
263
|
if not exclude_managed_identity_credential:
|
183
264
|
credentials.append(
|
184
265
|
ManagedIdentityCredential(
|
@@ -197,7 +278,7 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
197
278
|
except Exception as ex: # pylint:disable=broad-except
|
198
279
|
_LOGGER.info("Shared token cache is unavailable: '%s'", ex)
|
199
280
|
if not exclude_visual_studio_code_credential:
|
200
|
-
credentials.append(VisualStudioCodeCredential(
|
281
|
+
credentials.append(VisualStudioCodeCredential(tenant_id=vscode_tenant_id))
|
201
282
|
if not exclude_cli_credential:
|
202
283
|
credentials.append(AzureCliCredential(process_timeout=process_timeout))
|
203
284
|
if not exclude_powershell_credential:
|
@@ -213,6 +294,12 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
213
294
|
)
|
214
295
|
else:
|
215
296
|
credentials.append(InteractiveBrowserCredential(tenant_id=interactive_browser_tenant_id, **kwargs))
|
297
|
+
if not exclude_broker_credential:
|
298
|
+
broker_credential_args = {"tenant_id": broker_tenant_id, **kwargs}
|
299
|
+
if broker_client_id:
|
300
|
+
broker_credential_args["client_id"] = broker_client_id
|
301
|
+
credentials.append(BrokerCredential(**broker_credential_args))
|
302
|
+
|
216
303
|
within_dac.set(False)
|
217
304
|
super(DefaultAzureCredential, self).__init__(*credentials)
|
218
305
|
|
@@ -245,8 +332,10 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
245
332
|
)
|
246
333
|
return token
|
247
334
|
within_dac.set(True)
|
248
|
-
|
249
|
-
|
335
|
+
try:
|
336
|
+
token = super().get_token(*scopes, claims=claims, tenant_id=tenant_id, **kwargs)
|
337
|
+
finally:
|
338
|
+
within_dac.set(False)
|
250
339
|
return token
|
251
340
|
|
252
341
|
def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = None) -> AccessTokenInfo:
|
@@ -274,6 +363,8 @@ class DefaultAzureCredential(ChainedTokenCredential):
|
|
274
363
|
return token_info
|
275
364
|
|
276
365
|
within_dac.set(True)
|
277
|
-
|
278
|
-
|
366
|
+
try:
|
367
|
+
token_info = cast(SupportsTokenInfo, super()).get_token_info(*scopes, options=options)
|
368
|
+
finally:
|
369
|
+
within_dac.set(False)
|
279
370
|
return token_info
|
@@ -6,9 +6,11 @@ import os
|
|
6
6
|
import json
|
7
7
|
from typing import Any, Optional, Dict
|
8
8
|
|
9
|
+
from azure.core.pipeline import PipelineResponse
|
9
10
|
from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
|
10
11
|
from azure.core.pipeline.transport import HttpRequest
|
11
12
|
from azure.core.credentials import AccessTokenInfo
|
13
|
+
from azure.core.pipeline.policies import RetryPolicy
|
12
14
|
|
13
15
|
from .. import CredentialUnavailableError
|
14
16
|
from .._constants import EnvironmentVariables
|
@@ -31,6 +33,28 @@ PIPELINE_SETTINGS = {
|
|
31
33
|
}
|
32
34
|
|
33
35
|
|
36
|
+
class ImdsRetryPolicy(RetryPolicy):
|
37
|
+
"""Custom retry policy for IMDS credential with extended retry duration for 410 responses.
|
38
|
+
|
39
|
+
This policy ensures that specifically for 410 status codes, the total exponential backoff duration
|
40
|
+
is at least 70 seconds to handle temporary IMDS endpoint unavailability.
|
41
|
+
For other status codes, it uses the standard retry behavior.
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(self, **kwargs: Any) -> None:
|
45
|
+
# Increased backoff factor to ensure at least 70 seconds retry duration for 410 responses.
|
46
|
+
# Five retries, with each retry sleeping for [0.0s, 5.0s, 10.0s, 20.0s, 40.0s] between attempts (75s total)
|
47
|
+
self.backoff_factor_for_410 = 2.5
|
48
|
+
super().__init__(**kwargs)
|
49
|
+
|
50
|
+
def is_retry(self, settings: Dict[str, Any], response: PipelineResponse[Any, Any]) -> bool:
|
51
|
+
if response.http_response.status_code == 410:
|
52
|
+
settings["backoff"] = self.backoff_factor_for_410
|
53
|
+
else:
|
54
|
+
settings["backoff"] = self.backoff_factor
|
55
|
+
return super().is_retry(settings, response)
|
56
|
+
|
57
|
+
|
34
58
|
def _get_request(scope: str, identity_config: Dict) -> HttpRequest:
|
35
59
|
url = (
|
36
60
|
os.environ.get(EnvironmentVariables.AZURE_POD_IDENTITY_AUTHORITY_HOST, IMDS_AUTHORITY).strip("/")
|
@@ -58,7 +82,7 @@ def _check_forbidden_response(ex: HttpResponseError) -> None:
|
|
58
82
|
|
59
83
|
class ImdsCredential(MsalManagedIdentityClient):
|
60
84
|
def __init__(self, **kwargs: Any) -> None:
|
61
|
-
super(
|
85
|
+
super().__init__(retry_policy_class=ImdsRetryPolicy, **dict(PIPELINE_SETTINGS, **kwargs))
|
62
86
|
self._config = kwargs
|
63
87
|
|
64
88
|
if EnvironmentVariables.AZURE_POD_IDENTITY_AUTHORITY_HOST in os.environ:
|