Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 20 additions & 1 deletion src/managedcleanroom/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,23 @@ Release History
* Updated: Added --active-only filter to collaboration list and show commands
* Updated: Added --pending-only filter to invitation list command
* Updated: Added --scope, --from-seqno, --to-seqno filters to audit event list command
* Updated: Response structures modernized (many list endpoints now return structured objects with value arrays)
* Updated: Response structures modernized (many list endpoints now return structured objects with value arrays)

1.0.0b5
+++++++
* Updated to latest Frontend API spec (2026-03-01-preview with SKR policy)
* Regenerated analytics_frontend_api SDK with updated method signatures and SKR policy support
* BREAKING CHANGE: Removed `az managedcleanroom frontend analytics cleanroompolicy` command
* Added: `az managedcleanroom frontend analytics skr-policy` - Get SKR (Secure Key Release) policy for a specific dataset
- New required parameter: --dataset-id to specify the dataset for which to retrieve the SKR policy
* SDK Changes (internal):
- Added: collaboration.analytics_skr_policy_get(collaboration_id, dataset_id) method
- Removed: collaboration.analytics_cleanroompolicy_get(collaboration_id) method
- Fixed: analytics_queries_document_id_runhistory_get → analytics_queries_document_id_runs_get
- Fixed: analytics_queries_jobid_get → analytics_runs_job_id_get
* Bug Fixes:
- Fixed token normalization in _frontend_auth.py to handle tuple, AccessToken, and string token formats
- Added SSL verification environment variable support (AZURE_CLI_DISABLE_CONNECTION_VERIFICATION, REQUESTS_CA_BUNDLE)
- Fixed schema_file parameter handling in dataset publish to support Azure CLI auto-loading (dict, string, and @file formats)
- Fixed runhistory API endpoint method name
- Fixed runresult API endpoint method name
5 changes: 5 additions & 0 deletions src/managedcleanroom/azext_managedcleanroom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
from azure.cli.core import AzCommandsLoader
from azext_managedcleanroom._help import helps # pylint: disable=unused-import

try:
from azext_managedcleanroom import _breaking_change # noqa: F401 pylint: disable=unused-import
except ImportError:
pass


class ManagedcleanroomCommandsLoader(AzCommandsLoader):

Expand Down
13 changes: 13 additions & 0 deletions src/managedcleanroom/azext_managedcleanroom/_breaking_change.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.breaking_change import register_command_deprecate

# Register the deprecated command that was removed in v1.0.0b5
register_command_deprecate(
'managedcleanroom frontend analytics cleanroompolicy',
redirect='managedcleanroom frontend analytics skr-policy',
expiration='1.0.0b5'
)
122 changes: 84 additions & 38 deletions src/managedcleanroom/azext_managedcleanroom/_frontend_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ def get_frontend_token(cmd):

# Priority 0: explicit token via environment variable (for local/test envs
# only)
env_token = os.environ.get('MANAGEDCLEANROOM_ACCESS_TOKEN')
env_token = os.environ.get("MANAGEDCLEANROOM_ACCESS_TOKEN")
if env_token:
logger.warning(
"Using token from MANAGEDCLEANROOM_ACCESS_TOKEN env var FOR TESTING PURPOSES ONLY")
"Using token from MANAGEDCLEANROOM_ACCESS_TOKEN env var FOR TESTING PURPOSES ONLY"
)
from collections import namedtuple
AccessToken = namedtuple('AccessToken', ['token', 'expires_on'])

AccessToken = namedtuple("AccessToken", ["token", "expires_on"])
token_obj = AccessToken(token=env_token, expires_on=0)
return (token_obj, subscription, None)

Expand All @@ -49,20 +51,46 @@ def get_frontend_token(cmd):
msal_token = get_msal_token(cmd)
if msal_token:
logger.debug("Using MSAL device code flow token")
return (msal_token[0], subscription, msal_token[2])
from collections import namedtuple

AccessToken = namedtuple("AccessToken", ["token", "expires_on"])
token_obj = AccessToken(token=msal_token[0], expires_on=0)
return (token_obj, subscription, msal_token[2])

logger.debug("Using Azure CLI (az login) token")
return profile.get_raw_token(
subscription=subscription,
resource=auth_scope
raw_token = profile.get_raw_token(
subscription=subscription, resource=auth_scope
)

# Normalize to AccessToken object
from collections import namedtuple

AccessToken = namedtuple("AccessToken", ["token", "expires_on"])

# Handle different return types from get_raw_token
if isinstance(raw_token, tuple) and len(raw_token) >= 3:
# Tuple format: ('Bearer', 'token_string', {...})
# raw_token[2] should be a dict with metadata
metadata = raw_token[2] if isinstance(raw_token[2], dict) else {}
token_obj = AccessToken(
token=raw_token[1], expires_on=metadata.get("expiresOn", 0)
)
elif hasattr(raw_token, "token"):
# Already AccessToken object
token_obj = raw_token
else:
# Raw string token
token_obj = AccessToken(token=str(raw_token), expires_on=0)

return (token_obj, subscription, None)

except Exception as ex:
raise CLIError(
f'Failed to get access token: {str(ex)}\n\n'
'Please authenticate using one of:\n'
' 1. az managedcleanroom frontend login (MSAL device code flow)\n'
' 2. az login (Azure CLI authentication)\n')
f"Failed to get access token: {str(ex)}\n\n"
"Please authenticate using one of:\n"
" 1. az managedcleanroom frontend login (MSAL device code flow)\n"
" 2. az login (Azure CLI authentication)\n"
) from ex


def get_frontend_config(cmd):
Expand All @@ -73,7 +101,7 @@ def get_frontend_config(cmd):
:rtype: str or None
"""
config = cmd.cli_ctx.config
return config.get('managedcleanroom-frontend', 'endpoint', fallback=None)
return config.get("managedcleanroom-frontend", "endpoint", fallback=None)


def set_frontend_config(cmd, endpoint):
Expand All @@ -83,10 +111,7 @@ def set_frontend_config(cmd, endpoint):
:param endpoint: API endpoint URL to store
:type endpoint: str
"""
cmd.cli_ctx.config.set_value(
'managedcleanroom-frontend',
'endpoint',
endpoint)
cmd.cli_ctx.config.set_value("managedcleanroom-frontend", "endpoint", endpoint)


def get_frontend_client(cmd, endpoint=None, api_version=None):
Expand All @@ -104,33 +129,40 @@ def get_frontend_client(cmd, endpoint=None, api_version=None):
:raises: CLIError if token fetch fails or endpoint not configured
"""
from .analytics_frontend_api import AnalyticsFrontendAPI
from azure.core.pipeline.policies import BearerTokenCredentialPolicy, SansIOHTTPPolicy
from azure.core.pipeline.policies import (
BearerTokenCredentialPolicy,
SansIOHTTPPolicy,
)

# Use provided api_version or default
if api_version is None:
api_version = '2026-03-01-preview'
api_version = "2026-03-01-preview"

api_endpoint = endpoint or get_frontend_config(cmd)
if not api_endpoint:
raise CLIError(
'Analytics Frontend API endpoint not configured.\n'
'Configure using: az config set managedcleanroom-frontend.endpoint=<url>\n'
'Or use the --endpoint flag with your command.')
"Analytics Frontend API endpoint not configured.\n"
"Configure using: az config set managedcleanroom-frontend.endpoint=<url>\n"
"Or use the --endpoint flag with your command."
)

access_token_obj, _, _ = get_frontend_token(cmd)

logger.debug(
"Creating Analytics Frontend API client for endpoint: %s",
api_endpoint)
"Creating Analytics Frontend API client for endpoint: %s", api_endpoint
)

# Check if this is a local development endpoint
is_local = api_endpoint.startswith(
'http://localhost') or api_endpoint.startswith('http://127.0.0.1')
is_local = api_endpoint.startswith("http://localhost") or api_endpoint.startswith(
"http://127.0.0.1"
)

# Create simple credential wrapper for the access token
credential = type('TokenCredential', (), {
'get_token': lambda self, *args, **kwargs: access_token_obj
})()
credential = type(
"TokenCredential",
(),
{"get_token": lambda self, *args, **kwargs: access_token_obj},
)()

if is_local:
# For local development, create a custom auth policy that bypasses
Expand All @@ -146,7 +178,7 @@ def on_request(self, request):
# Extract token string from AccessToken object
# The token might be a tuple ('Bearer', 'token_string') or just
# the token string
if hasattr(self._token, 'token'):
if hasattr(self._token, "token"):
token_value = self._token.token
else:
token_value = self._token
Expand All @@ -157,29 +189,43 @@ def on_request(self, request):
else:
token_string = str(token_value)

auth_header = f'Bearer {token_string}'
auth_header = f"Bearer {token_string}"
logger.debug(
"Setting Authorization header: Bearer %s...", token_string[:50])
request.http_request.headers['Authorization'] = auth_header
"Setting Authorization header: Bearer %s...", token_string[:50]
)
request.http_request.headers["Authorization"] = auth_header

auth_policy = LocalBearerTokenPolicy(access_token_obj)
else:
# For production, use standard bearer token policy with HTTPS
# enforcement
# Use configured auth_scope with .default suffix for Azure SDK
from ._msal_auth import get_auth_scope

scope = get_auth_scope(cmd)
if not scope.endswith('/.default'):
scope = f'{scope}/.default'
if not scope.endswith("/.default"):
scope = f"{scope}/.default"

auth_policy = BearerTokenCredentialPolicy(
credential,
scope
auth_policy = BearerTokenCredentialPolicy(credential, scope)

# Handle SSL verification settings
import os

client_kwargs = {}
if os.environ.get("AZURE_CLI_DISABLE_CONNECTION_VERIFICATION"):
logger.warning(
"SSL verification disabled via AZURE_CLI_DISABLE_CONNECTION_VERIFICATION"
)
client_kwargs["connection_verify"] = False
elif os.environ.get("REQUESTS_CA_BUNDLE"):
ca_bundle = os.environ["REQUESTS_CA_BUNDLE"]
logger.debug("Using custom CA bundle: %s", ca_bundle)
client_kwargs["connection_verify"] = ca_bundle

# Return configured client
return AnalyticsFrontendAPI(
endpoint=api_endpoint,
api_version=api_version,
authentication_policy=auth_policy
authentication_policy=auth_policy,
**client_kwargs,
)
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def load_frontend_command_table(loader, _):
with loader.command_group('managedcleanroom frontend analytics', custom_command_type=frontend_custom) as g:
g.custom_show_command('show', 'frontend_collaboration_analytics_show')
g.custom_command(
'cleanroompolicy',
'frontend_collaboration_analytics_cleanroompolicy')
'skr-policy',
'frontend_collaboration_analytics_skr_policy')

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