Skip to content

Commit feca247

Browse files
sakshamgargMSSaksham Garg
andauthored
Update to v1.0.0b5 with SKR policy and critical bug fixes (#9740)
* Update to v1.0.0b5 with SKR policy and critical bug fixes * Add breaking change registration for deprecated cleanroompolicy command - Register cleanroompolicy command as deprecated with redirect to skr-policy - This acknowledges the breaking change introduced in v1.0.0b5 - Required for Azure CLI Extensions CI breaking change test * Fix test mocks to accept encoding parameter for Python 3.10-3.12 compatibility - Updated mock_open_handler in test_publish_query_with_parameters_from_files to accept **kwargs - Added proper file mock for test_publish_query_disallows_mixed_segments - Fixed assertion text to match actual error message - All 54 tests now passing on Python 3.10, 3.11, 3.12, and 3.13 --------- Co-authored-by: Saksham Garg <sakshamgarg@microsoft.com>
1 parent db11aca commit feca247

29 files changed

+2209
-1021
lines changed

src/managedcleanroom/HISTORY.rst

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,23 @@ Release History
4646
* Updated: Added --active-only filter to collaboration list and show commands
4747
* Updated: Added --pending-only filter to invitation list command
4848
* Updated: Added --scope, --from-seqno, --to-seqno filters to audit event list command
49-
* Updated: Response structures modernized (many list endpoints now return structured objects with value arrays)
49+
* Updated: Response structures modernized (many list endpoints now return structured objects with value arrays)
50+
51+
1.0.0b5
52+
+++++++
53+
* Updated to latest Frontend API spec (2026-03-01-preview with SKR policy)
54+
* Regenerated analytics_frontend_api SDK with updated method signatures and SKR policy support
55+
* BREAKING CHANGE: Removed `az managedcleanroom frontend analytics cleanroompolicy` command
56+
* Added: `az managedcleanroom frontend analytics skr-policy` - Get SKR (Secure Key Release) policy for a specific dataset
57+
- New required parameter: --dataset-id to specify the dataset for which to retrieve the SKR policy
58+
* SDK Changes (internal):
59+
- Added: collaboration.analytics_skr_policy_get(collaboration_id, dataset_id) method
60+
- Removed: collaboration.analytics_cleanroompolicy_get(collaboration_id) method
61+
- Fixed: analytics_queries_document_id_runhistory_get → analytics_queries_document_id_runs_get
62+
- Fixed: analytics_queries_jobid_get → analytics_runs_job_id_get
63+
* Bug Fixes:
64+
- Fixed token normalization in _frontend_auth.py to handle tuple, AccessToken, and string token formats
65+
- Added SSL verification environment variable support (AZURE_CLI_DISABLE_CONNECTION_VERIFICATION, REQUESTS_CA_BUNDLE)
66+
- Fixed schema_file parameter handling in dataset publish to support Azure CLI auto-loading (dict, string, and @file formats)
67+
- Fixed runhistory API endpoint method name
68+
- Fixed runresult API endpoint method name

src/managedcleanroom/azext_managedcleanroom/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
from azure.cli.core import AzCommandsLoader
1010
from azext_managedcleanroom._help import helps # pylint: disable=unused-import
1111

12+
try:
13+
from azext_managedcleanroom import _breaking_change # noqa: F401 pylint: disable=unused-import
14+
except ImportError:
15+
pass
16+
1217

1318
class ManagedcleanroomCommandsLoader(AzCommandsLoader):
1419

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
from azure.cli.core.breaking_change import register_command_deprecate
7+
8+
# Register the deprecated command that was removed in v1.0.0b5
9+
register_command_deprecate(
10+
'managedcleanroom frontend analytics cleanroompolicy',
11+
redirect='managedcleanroom frontend analytics skr-policy',
12+
expiration='1.0.0b5'
13+
)

src/managedcleanroom/azext_managedcleanroom/_frontend_auth.py

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ def get_frontend_token(cmd):
3232

3333
# Priority 0: explicit token via environment variable (for local/test envs
3434
# only)
35-
env_token = os.environ.get('MANAGEDCLEANROOM_ACCESS_TOKEN')
35+
env_token = os.environ.get("MANAGEDCLEANROOM_ACCESS_TOKEN")
3636
if env_token:
3737
logger.warning(
38-
"Using token from MANAGEDCLEANROOM_ACCESS_TOKEN env var FOR TESTING PURPOSES ONLY")
38+
"Using token from MANAGEDCLEANROOM_ACCESS_TOKEN env var FOR TESTING PURPOSES ONLY"
39+
)
3940
from collections import namedtuple
40-
AccessToken = namedtuple('AccessToken', ['token', 'expires_on'])
41+
42+
AccessToken = namedtuple("AccessToken", ["token", "expires_on"])
4143
token_obj = AccessToken(token=env_token, expires_on=0)
4244
return (token_obj, subscription, None)
4345

@@ -49,20 +51,46 @@ def get_frontend_token(cmd):
4951
msal_token = get_msal_token(cmd)
5052
if msal_token:
5153
logger.debug("Using MSAL device code flow token")
52-
return (msal_token[0], subscription, msal_token[2])
54+
from collections import namedtuple
55+
56+
AccessToken = namedtuple("AccessToken", ["token", "expires_on"])
57+
token_obj = AccessToken(token=msal_token[0], expires_on=0)
58+
return (token_obj, subscription, msal_token[2])
5359

5460
logger.debug("Using Azure CLI (az login) token")
55-
return profile.get_raw_token(
56-
subscription=subscription,
57-
resource=auth_scope
61+
raw_token = profile.get_raw_token(
62+
subscription=subscription, resource=auth_scope
5863
)
5964

65+
# Normalize to AccessToken object
66+
from collections import namedtuple
67+
68+
AccessToken = namedtuple("AccessToken", ["token", "expires_on"])
69+
70+
# Handle different return types from get_raw_token
71+
if isinstance(raw_token, tuple) and len(raw_token) >= 3:
72+
# Tuple format: ('Bearer', 'token_string', {...})
73+
# raw_token[2] should be a dict with metadata
74+
metadata = raw_token[2] if isinstance(raw_token[2], dict) else {}
75+
token_obj = AccessToken(
76+
token=raw_token[1], expires_on=metadata.get("expiresOn", 0)
77+
)
78+
elif hasattr(raw_token, "token"):
79+
# Already AccessToken object
80+
token_obj = raw_token
81+
else:
82+
# Raw string token
83+
token_obj = AccessToken(token=str(raw_token), expires_on=0)
84+
85+
return (token_obj, subscription, None)
86+
6087
except Exception as ex:
6188
raise CLIError(
62-
f'Failed to get access token: {str(ex)}\n\n'
63-
'Please authenticate using one of:\n'
64-
' 1. az managedcleanroom frontend login (MSAL device code flow)\n'
65-
' 2. az login (Azure CLI authentication)\n')
89+
f"Failed to get access token: {str(ex)}\n\n"
90+
"Please authenticate using one of:\n"
91+
" 1. az managedcleanroom frontend login (MSAL device code flow)\n"
92+
" 2. az login (Azure CLI authentication)\n"
93+
) from ex
6694

6795

6896
def get_frontend_config(cmd):
@@ -73,7 +101,7 @@ def get_frontend_config(cmd):
73101
:rtype: str or None
74102
"""
75103
config = cmd.cli_ctx.config
76-
return config.get('managedcleanroom-frontend', 'endpoint', fallback=None)
104+
return config.get("managedcleanroom-frontend", "endpoint", fallback=None)
77105

78106

79107
def set_frontend_config(cmd, endpoint):
@@ -83,10 +111,7 @@ def set_frontend_config(cmd, endpoint):
83111
:param endpoint: API endpoint URL to store
84112
:type endpoint: str
85113
"""
86-
cmd.cli_ctx.config.set_value(
87-
'managedcleanroom-frontend',
88-
'endpoint',
89-
endpoint)
114+
cmd.cli_ctx.config.set_value("managedcleanroom-frontend", "endpoint", endpoint)
90115

91116

92117
def get_frontend_client(cmd, endpoint=None, api_version=None):
@@ -104,33 +129,40 @@ def get_frontend_client(cmd, endpoint=None, api_version=None):
104129
:raises: CLIError if token fetch fails or endpoint not configured
105130
"""
106131
from .analytics_frontend_api import AnalyticsFrontendAPI
107-
from azure.core.pipeline.policies import BearerTokenCredentialPolicy, SansIOHTTPPolicy
132+
from azure.core.pipeline.policies import (
133+
BearerTokenCredentialPolicy,
134+
SansIOHTTPPolicy,
135+
)
108136

109137
# Use provided api_version or default
110138
if api_version is None:
111-
api_version = '2026-03-01-preview'
139+
api_version = "2026-03-01-preview"
112140

113141
api_endpoint = endpoint or get_frontend_config(cmd)
114142
if not api_endpoint:
115143
raise CLIError(
116-
'Analytics Frontend API endpoint not configured.\n'
117-
'Configure using: az config set managedcleanroom-frontend.endpoint=<url>\n'
118-
'Or use the --endpoint flag with your command.')
144+
"Analytics Frontend API endpoint not configured.\n"
145+
"Configure using: az config set managedcleanroom-frontend.endpoint=<url>\n"
146+
"Or use the --endpoint flag with your command."
147+
)
119148

120149
access_token_obj, _, _ = get_frontend_token(cmd)
121150

122151
logger.debug(
123-
"Creating Analytics Frontend API client for endpoint: %s",
124-
api_endpoint)
152+
"Creating Analytics Frontend API client for endpoint: %s", api_endpoint
153+
)
125154

126155
# Check if this is a local development endpoint
127-
is_local = api_endpoint.startswith(
128-
'http://localhost') or api_endpoint.startswith('http://127.0.0.1')
156+
is_local = api_endpoint.startswith("http://localhost") or api_endpoint.startswith(
157+
"http://127.0.0.1"
158+
)
129159

130160
# Create simple credential wrapper for the access token
131-
credential = type('TokenCredential', (), {
132-
'get_token': lambda self, *args, **kwargs: access_token_obj
133-
})()
161+
credential = type(
162+
"TokenCredential",
163+
(),
164+
{"get_token": lambda self, *args, **kwargs: access_token_obj},
165+
)()
134166

135167
if is_local:
136168
# For local development, create a custom auth policy that bypasses
@@ -146,7 +178,7 @@ def on_request(self, request):
146178
# Extract token string from AccessToken object
147179
# The token might be a tuple ('Bearer', 'token_string') or just
148180
# the token string
149-
if hasattr(self._token, 'token'):
181+
if hasattr(self._token, "token"):
150182
token_value = self._token.token
151183
else:
152184
token_value = self._token
@@ -157,29 +189,43 @@ def on_request(self, request):
157189
else:
158190
token_string = str(token_value)
159191

160-
auth_header = f'Bearer {token_string}'
192+
auth_header = f"Bearer {token_string}"
161193
logger.debug(
162-
"Setting Authorization header: Bearer %s...", token_string[:50])
163-
request.http_request.headers['Authorization'] = auth_header
194+
"Setting Authorization header: Bearer %s...", token_string[:50]
195+
)
196+
request.http_request.headers["Authorization"] = auth_header
164197

165198
auth_policy = LocalBearerTokenPolicy(access_token_obj)
166199
else:
167200
# For production, use standard bearer token policy with HTTPS
168201
# enforcement
169202
# Use configured auth_scope with .default suffix for Azure SDK
170203
from ._msal_auth import get_auth_scope
204+
171205
scope = get_auth_scope(cmd)
172-
if not scope.endswith('/.default'):
173-
scope = f'{scope}/.default'
206+
if not scope.endswith("/.default"):
207+
scope = f"{scope}/.default"
174208

175-
auth_policy = BearerTokenCredentialPolicy(
176-
credential,
177-
scope
209+
auth_policy = BearerTokenCredentialPolicy(credential, scope)
210+
211+
# Handle SSL verification settings
212+
import os
213+
214+
client_kwargs = {}
215+
if os.environ.get("AZURE_CLI_DISABLE_CONNECTION_VERIFICATION"):
216+
logger.warning(
217+
"SSL verification disabled via AZURE_CLI_DISABLE_CONNECTION_VERIFICATION"
178218
)
219+
client_kwargs["connection_verify"] = False
220+
elif os.environ.get("REQUESTS_CA_BUNDLE"):
221+
ca_bundle = os.environ["REQUESTS_CA_BUNDLE"]
222+
logger.debug("Using custom CA bundle: %s", ca_bundle)
223+
client_kwargs["connection_verify"] = ca_bundle
179224

180225
# Return configured client
181226
return AnalyticsFrontendAPI(
182227
endpoint=api_endpoint,
183228
api_version=api_version,
184-
authentication_policy=auth_policy
229+
authentication_policy=auth_policy,
230+
**client_kwargs,
185231
)

src/managedcleanroom/azext_managedcleanroom/_frontend_commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def load_frontend_command_table(loader, _):
3232
with loader.command_group('managedcleanroom frontend analytics', custom_command_type=frontend_custom) as g:
3333
g.custom_show_command('show', 'frontend_collaboration_analytics_show')
3434
g.custom_command(
35-
'cleanroompolicy',
36-
'frontend_collaboration_analytics_cleanroompolicy')
35+
'skr-policy',
36+
'frontend_collaboration_analytics_skr_policy')
3737

3838
# OIDC commands
3939
with loader.command_group('managedcleanroom frontend oidc', custom_command_type=frontend_custom) as g:

0 commit comments

Comments
 (0)