Skip to content

Commit 35b686e

Browse files
Feature managedcleanroom frontend (#9572)
1 parent f72da2d commit 35b686e

33 files changed

+13440
-13
lines changed

src/managedcleanroom/HISTORY.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@ Release History
55

66
1.0.0b1
77
++++++
8-
* Initial release.
8+
* Initial release.
9+
10+
1.0.0b2
11+
++++++
12+
* Add frontend commandlets
13+
* Add MSAL device code flow authentication

src/managedcleanroom/README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,47 @@ az managedcleanroom consortium create \
2121
--location westus
2222
```
2323

24-
If you have issues, please give feedback by opening an issue at https://github.com/Azure/azure-cli-extensions/issues.
24+
## SDK Regeneration Guide ##
25+
26+
When regenerating `analytics_frontend_api/` SDK using autorest:
27+
28+
```bash
29+
autorest --input-file=../frontend.yaml --python --output-folder=./generated-cmdlets-frontend
30+
```
31+
32+
Update the following files to match SDK changes:
33+
34+
### 1. Map SDK Method Names
35+
**File:** `_frontend_custom.py`
36+
- Update `client.collaboration.<old_method>()``client.collaboration.<new_method>()`
37+
- Update function parameter names to match SDK
38+
39+
### 2. Update Parameter Definitions
40+
**File:** `_params.py`
41+
- Rename argument types (e.g., `query_id_type``document_id_type`)
42+
- Update `options_list` and `help` text
43+
- Add/remove parameter contexts in `with self.argument_context()` blocks
44+
45+
### 3. Update Command Registration
46+
**File:** `_frontend_commands.py`
47+
- Update command group paths if SDK URLs changed
48+
- Update function names in `g.custom_command()` calls
49+
50+
### 4. Update Help Documentation
51+
**File:** `_help.py`
52+
- Update parameter names in examples
53+
- Update command paths for new/renamed commands
54+
- Add help entries for new commands
55+
56+
### 5. Update Unit Tests
57+
**Files:** `tests/latest/test_frontend_*.py`
58+
- Update mock paths: `mock_client.collaboration.<old_method>``<new_method>`
59+
- Update function parameter names in test calls
60+
- Update function imports if renamed
61+
62+
### 6. Validate Changes
63+
```bash
64+
# Run tests
65+
cd src/managedcleanroom
66+
python -m pytest azext_managedcleanroom/tests/latest/ -v
67+
```

src/managedcleanroom/azext_managedcleanroom/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#
55
# Code generated by aaz-dev-tools
66
# --------------------------------------------------------------------------------------------
7+
# flake8: noqa
78

89
from azure.cli.core import AzCommandsLoader
910
from azext_managedcleanroom._help import helps # pylint: disable=unused-import
@@ -20,6 +21,7 @@ def __init__(self, cli_ctx=None):
2021

2122
def load_command_table(self, args):
2223
from azext_managedcleanroom.commands import load_command_table
24+
from azext_managedcleanroom._frontend_commands import load_frontend_command_table
2325
from azure.cli.core.aaz import load_aaz_command_table
2426
try:
2527
from . import aaz
@@ -32,6 +34,7 @@ def load_command_table(self, args):
3234
args=args
3335
)
3436
load_command_table(self, args)
37+
load_frontend_command_table(self, args)
3538
return self.command_table
3639

3740
def load_arguments(self, command):
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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+
"""Authentication and client management for Analytics Frontend API"""
7+
8+
from azure.cli.core._profile import Profile
9+
from azure.cli.core.commands.client_factory import get_subscription_id
10+
from azure.cli.core.util import CLIError
11+
from knack.log import get_logger
12+
13+
logger = get_logger(__name__)
14+
15+
16+
def get_frontend_token(cmd):
17+
"""Get access token from MSAL cache or Azure context
18+
19+
Authentication priority:
20+
1. MSAL token (device code flow) - if user ran 'frontend login'
21+
2. Azure CLI token (az login) - fallback
22+
23+
:param cmd: CLI command context
24+
:return: Tuple of (access_token, subscription, tenant)
25+
:raises: CLIError if token cannot be obtained
26+
"""
27+
from ._msal_auth import get_msal_token, get_auth_scope
28+
29+
profile = Profile(cli_ctx=cmd.cli_ctx)
30+
subscription = get_subscription_id(cmd.cli_ctx)
31+
auth_scope = get_auth_scope(cmd)
32+
33+
logger.debug("Using auth scope: %s", auth_scope)
34+
35+
try:
36+
msal_token = get_msal_token(cmd)
37+
if msal_token:
38+
logger.debug("Using MSAL device code flow token")
39+
return (msal_token[0], subscription, msal_token[2])
40+
41+
logger.debug("Using Azure CLI (az login) token")
42+
return profile.get_raw_token(
43+
subscription=subscription,
44+
resource=auth_scope
45+
)
46+
47+
except Exception as ex:
48+
raise CLIError(
49+
f'Failed to get access token: {str(ex)}\n\n'
50+
'Please authenticate using one of:\n'
51+
' 1. az managedcleanroom frontend login (MSAL device code flow)\n'
52+
' 2. az login (Azure CLI authentication)\n')
53+
54+
55+
def get_frontend_config(cmd):
56+
"""Read frontend endpoint configuration from Azure CLI config
57+
58+
:param cmd: CLI command context
59+
:return: Configured endpoint URL or None
60+
:rtype: str or None
61+
"""
62+
config = cmd.cli_ctx.config
63+
return config.get('managedcleanroom-frontend', 'endpoint', fallback=None)
64+
65+
66+
def set_frontend_config(cmd, endpoint):
67+
"""Store frontend endpoint in Azure CLI config
68+
69+
:param cmd: CLI command context
70+
:param endpoint: API endpoint URL to store
71+
:type endpoint: str
72+
"""
73+
cmd.cli_ctx.config.set_value(
74+
'managedcleanroom-frontend',
75+
'endpoint',
76+
endpoint)
77+
78+
79+
def get_frontend_client(cmd, endpoint=None):
80+
"""Create Analytics Frontend API client with Azure authentication
81+
82+
Uses Profile.get_raw_token() to fetch access token from Azure context.
83+
Token is fetched fresh on every invocation.
84+
85+
:param cmd: CLI command context
86+
:param endpoint: Optional explicit endpoint URL (overrides config)
87+
:type endpoint: str
88+
:return: Configured AnalyticsFrontendAPI client
89+
:raises: CLIError if token fetch fails or endpoint not configured
90+
"""
91+
from .analytics_frontend_api import AnalyticsFrontendAPI
92+
from azure.core.pipeline.policies import BearerTokenCredentialPolicy, SansIOHTTPPolicy
93+
94+
api_endpoint = endpoint or get_frontend_config(cmd)
95+
if not api_endpoint:
96+
raise CLIError(
97+
'Analytics Frontend API endpoint not configured.\n'
98+
'Configure using: az config set managedcleanroom-frontend.endpoint=<url>\n'
99+
'Or use the --endpoint flag with your command.')
100+
101+
access_token_obj, _, _ = get_frontend_token(cmd)
102+
103+
logger.debug(
104+
"Creating Analytics Frontend API client for endpoint: %s",
105+
api_endpoint)
106+
107+
# Check if this is a local development endpoint
108+
is_local = api_endpoint.startswith(
109+
'http://localhost') or api_endpoint.startswith('http://127.0.0.1')
110+
111+
# Create simple credential wrapper for the access token
112+
credential = type('TokenCredential', (), {
113+
'get_token': lambda self, *args, **kwargs: access_token_obj
114+
})()
115+
116+
if is_local:
117+
# For local development, create a custom auth policy that bypasses
118+
# HTTPS check
119+
class LocalBearerTokenPolicy(SansIOHTTPPolicy):
120+
"""Bearer token policy that allows HTTP for local development"""
121+
122+
def __init__(self, token_obj):
123+
self._token = token_obj # AccessToken object
124+
125+
def on_request(self, request):
126+
"""Add authorization header with bearer token"""
127+
# Extract token string from AccessToken object
128+
# The token might be a tuple ('Bearer', 'token_string') or just
129+
# the token string
130+
if hasattr(self._token, 'token'):
131+
token_value = self._token.token
132+
else:
133+
token_value = self._token
134+
135+
# If it's a tuple, extract the actual token (second element)
136+
if isinstance(token_value, tuple) and len(token_value) >= 2:
137+
token_string = token_value[1]
138+
else:
139+
token_string = str(token_value)
140+
141+
auth_header = f'Bearer {token_string}'
142+
logger.debug(
143+
"Setting Authorization header: Bearer %s...", token_string[:50])
144+
request.http_request.headers['Authorization'] = auth_header
145+
146+
auth_policy = LocalBearerTokenPolicy(access_token_obj)
147+
else:
148+
# For production, use standard bearer token policy with HTTPS
149+
# enforcement
150+
# Use configured auth_scope with .default suffix for Azure SDK
151+
from ._msal_auth import get_auth_scope
152+
scope = get_auth_scope(cmd)
153+
if not scope.endswith('/.default'):
154+
scope = f'{scope}/.default'
155+
156+
auth_policy = BearerTokenCredentialPolicy(
157+
credential,
158+
scope
159+
)
160+
161+
# Return configured client
162+
return AnalyticsFrontendAPI(
163+
endpoint=api_endpoint,
164+
authentication_policy=auth_policy
165+
)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
"""Command registration for Analytics Frontend API"""
7+
8+
from azure.cli.core.commands import CliCommandType
9+
10+
11+
def load_frontend_command_table(loader, _):
12+
"""Register all Analytics Frontend API commands
13+
14+
Registers 26 commands across command groups for frontend collaboration.
15+
16+
:param loader: Command loader instance
17+
"""
18+
19+
frontend_custom = CliCommandType(
20+
operations_tmpl='azext_managedcleanroom._frontend_custom#{}')
21+
22+
# Base collaboration commands (only list - no --collaboration-id needed)
23+
with loader.command_group('managedcleanroom frontend collaboration', custom_command_type=frontend_custom) as g:
24+
g.custom_command('list', 'frontend_collaboration_list')
25+
26+
# Show command at frontend level (requires --collaboration-id)
27+
with loader.command_group('managedcleanroom frontend', custom_command_type=frontend_custom) as g:
28+
g.custom_show_command('show', 'frontend_collaboration_show')
29+
30+
# Workloads commands
31+
with loader.command_group('managedcleanroom frontend workloads', custom_command_type=frontend_custom) as g:
32+
g.custom_command('list', 'frontend_collaboration_workloads_list')
33+
34+
# Analytics commands
35+
with loader.command_group('managedcleanroom frontend analytics', custom_command_type=frontend_custom) as g:
36+
g.custom_show_command('show', 'frontend_collaboration_analytics_show')
37+
g.custom_command(
38+
'deploymentinfo',
39+
'frontend_collaboration_analytics_deploymentinfo')
40+
g.custom_command(
41+
'cleanroompolicy',
42+
'frontend_collaboration_analytics_cleanroompolicy')
43+
44+
# OIDC commands
45+
with loader.command_group('managedcleanroom frontend oidc issuerinfo', custom_command_type=frontend_custom) as g:
46+
g.custom_show_command(
47+
'show', 'frontend_collaboration_oidc_issuerinfo_show')
48+
49+
# Invitation commands
50+
with loader.command_group('managedcleanroom frontend invitation', custom_command_type=frontend_custom) as g:
51+
g.custom_command('list', 'frontend_collaboration_invitation_list')
52+
g.custom_show_command('show', 'frontend_collaboration_invitation_show')
53+
g.custom_command('accept', 'frontend_collaboration_invitation_accept')
54+
55+
# Dataset commands
56+
with loader.command_group('managedcleanroom frontend analytics dataset', custom_command_type=frontend_custom) as g:
57+
g.custom_command('list', 'frontend_collaboration_dataset_list')
58+
g.custom_show_command('show', 'frontend_collaboration_dataset_show')
59+
g.custom_command('publish', 'frontend_collaboration_dataset_publish')
60+
61+
# Consent commands
62+
with loader.command_group('managedcleanroom frontend consent', custom_command_type=frontend_custom) as g:
63+
g.custom_command('check', 'frontend_collaboration_consent_check')
64+
g.custom_command('set', 'frontend_collaboration_consent_set')
65+
66+
# Query commands
67+
with loader.command_group('managedcleanroom frontend analytics query', custom_command_type=frontend_custom) as g:
68+
g.custom_command('list', 'frontend_collaboration_query_list')
69+
g.custom_show_command('show', 'frontend_collaboration_query_show')
70+
g.custom_command('publish', 'frontend_collaboration_query_publish')
71+
g.custom_command('run', 'frontend_collaboration_query_run')
72+
73+
# Query vote commands
74+
with loader.command_group(
75+
'managedcleanroom frontend analytics query vote',
76+
custom_command_type=frontend_custom) as g:
77+
g.custom_command('accept', 'frontend_collaboration_query_vote_accept')
78+
g.custom_command('reject', 'frontend_collaboration_query_vote_reject')
79+
80+
# Query run history commands
81+
with loader.command_group(
82+
'managedcleanroom frontend analytics query runhistory',
83+
custom_command_type=frontend_custom) as g:
84+
g.custom_command(
85+
'list', 'frontend_collaboration_query_runhistory_list')
86+
87+
# Query run result commands
88+
with loader.command_group(
89+
'managedcleanroom frontend analytics query runresult',
90+
custom_command_type=frontend_custom) as g:
91+
g.custom_show_command(
92+
'show', 'frontend_collaboration_query_runresult_show')
93+
94+
# Audit event commands
95+
with loader.command_group(
96+
'managedcleanroom frontend analytics auditevent',
97+
custom_command_type=frontend_custom) as g:
98+
g.custom_command('list', 'frontend_collaboration_audit_list')
99+
100+
# Attestation commands
101+
with loader.command_group('managedcleanroom frontend attestation', custom_command_type=frontend_custom) as g:
102+
g.custom_command('cgs', 'frontend_collaboration_attestation_cgs')
103+
104+
with loader.command_group(
105+
'managedcleanroom frontend analytics attestationreport',
106+
custom_command_type=frontend_custom) as g:
107+
g.custom_command(
108+
'cleanroom',
109+
'frontend_collaboration_attestation_cleanroom')
110+
111+
# Configuration and authentication commands
112+
with loader.command_group('managedcleanroom frontend', custom_command_type=frontend_custom) as g:
113+
g.custom_command('configure', 'frontend_configure')
114+
g.custom_command('login', 'frontend_login')
115+
g.custom_command('logout', 'frontend_logout')

0 commit comments

Comments
 (0)