-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Feature managedcleanroom frontend #9572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
yanzhudd
merged 14 commits into
Azure:main
from
sakshamgargMS:feature-managedcleanroom-frontend
Feb 13, 2026
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
250fb88
[cleanroom frontend cmdlet] initial commit
84d44ad
Add unittests
2b90928
Refactor commands to remove collaboration
b1d7fc6
Fix linting , bump version
b950358
Formatting fixes, add body for POST calls
179fd69
refactor out workloads, add body, linting, test fixes
1e2717b
Refactor query datasets and attestation report under analytics
9abc32d
style and linting
d40ee63
Version change and linting
56ccd67
Audit list -> auditevent list
c960b8c
Make auth scope endpoint configurable
d9c4634
License headers for sdk
e104cba
update docs for regeneration of sdk
1af257a
Fix license
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
165 changes: 165 additions & 0 deletions
165
src/managedcleanroom/azext_managedcleanroom/_frontend_auth.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| """Authentication and client management for Analytics Frontend API""" | ||
|
|
||
| from azure.cli.core._profile import Profile | ||
| from azure.cli.core.commands.client_factory import get_subscription_id | ||
| from azure.cli.core.util import CLIError | ||
| from knack.log import get_logger | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
| def get_frontend_token(cmd): | ||
| """Get access token from MSAL cache or Azure context | ||
|
|
||
| Authentication priority: | ||
| 1. MSAL token (device code flow) - if user ran 'frontend login' | ||
| 2. Azure CLI token (az login) - fallback | ||
|
|
||
| :param cmd: CLI command context | ||
| :return: Tuple of (access_token, subscription, tenant) | ||
| :raises: CLIError if token cannot be obtained | ||
| """ | ||
| from ._msal_auth import get_msal_token, get_auth_scope | ||
|
|
||
| profile = Profile(cli_ctx=cmd.cli_ctx) | ||
| subscription = get_subscription_id(cmd.cli_ctx) | ||
| auth_scope = get_auth_scope(cmd) | ||
|
|
||
| logger.debug("Using auth scope: %s", auth_scope) | ||
|
|
||
| try: | ||
| 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]) | ||
|
|
||
| logger.debug("Using Azure CLI (az login) token") | ||
| return profile.get_raw_token( | ||
| subscription=subscription, | ||
| resource=auth_scope | ||
| ) | ||
|
|
||
| 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') | ||
|
|
||
|
|
||
| def get_frontend_config(cmd): | ||
| """Read frontend endpoint configuration from Azure CLI config | ||
|
|
||
| :param cmd: CLI command context | ||
| :return: Configured endpoint URL or None | ||
| :rtype: str or None | ||
| """ | ||
| config = cmd.cli_ctx.config | ||
| return config.get('managedcleanroom-frontend', 'endpoint', fallback=None) | ||
|
|
||
|
|
||
| def set_frontend_config(cmd, endpoint): | ||
| """Store frontend endpoint in Azure CLI config | ||
|
|
||
| :param cmd: CLI command context | ||
| :param endpoint: API endpoint URL to store | ||
| :type endpoint: str | ||
| """ | ||
| cmd.cli_ctx.config.set_value( | ||
| 'managedcleanroom-frontend', | ||
| 'endpoint', | ||
| endpoint) | ||
|
|
||
|
|
||
| def get_frontend_client(cmd, endpoint=None): | ||
| """Create Analytics Frontend API client with Azure authentication | ||
|
|
||
| Uses Profile.get_raw_token() to fetch access token from Azure context. | ||
| Token is fetched fresh on every invocation. | ||
|
|
||
| :param cmd: CLI command context | ||
| :param endpoint: Optional explicit endpoint URL (overrides config) | ||
| :type endpoint: str | ||
| :return: Configured AnalyticsFrontendAPI client | ||
| :raises: CLIError if token fetch fails or endpoint not configured | ||
| """ | ||
| from .analytics_frontend_api import AnalyticsFrontendAPI | ||
| from azure.core.pipeline.policies import BearerTokenCredentialPolicy, SansIOHTTPPolicy | ||
|
|
||
| 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.') | ||
|
|
||
| access_token_obj, _, _ = get_frontend_token(cmd) | ||
|
|
||
| logger.debug( | ||
| "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') | ||
|
|
||
| # Create simple credential wrapper for the access token | ||
| 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 | ||
| # HTTPS check | ||
| class LocalBearerTokenPolicy(SansIOHTTPPolicy): | ||
| """Bearer token policy that allows HTTP for local development""" | ||
|
|
||
| def __init__(self, token_obj): | ||
| self._token = token_obj # AccessToken object | ||
|
|
||
| def on_request(self, request): | ||
| """Add authorization header with bearer token""" | ||
| # 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'): | ||
| token_value = self._token.token | ||
| else: | ||
| token_value = self._token | ||
|
|
||
| # If it's a tuple, extract the actual token (second element) | ||
| if isinstance(token_value, tuple) and len(token_value) >= 2: | ||
| token_string = token_value[1] | ||
| else: | ||
| token_string = str(token_value) | ||
|
|
||
| auth_header = f'Bearer {token_string}' | ||
| logger.debug( | ||
| "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' | ||
|
|
||
| auth_policy = BearerTokenCredentialPolicy( | ||
| credential, | ||
| scope | ||
| ) | ||
|
|
||
| # Return configured client | ||
| return AnalyticsFrontendAPI( | ||
| endpoint=api_endpoint, | ||
| authentication_policy=auth_policy | ||
| ) |
115 changes: 115 additions & 0 deletions
115
src/managedcleanroom/azext_managedcleanroom/_frontend_commands.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # -------------------------------------------------------------------------------------------- | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. See License.txt in the project root for license information. | ||
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| """Command registration for Analytics Frontend API""" | ||
|
|
||
| from azure.cli.core.commands import CliCommandType | ||
|
|
||
|
|
||
| def load_frontend_command_table(loader, _): | ||
| """Register all Analytics Frontend API commands | ||
|
|
||
| Registers 26 commands across command groups for frontend collaboration. | ||
|
|
||
| :param loader: Command loader instance | ||
| """ | ||
|
|
||
| frontend_custom = CliCommandType( | ||
| operations_tmpl='azext_managedcleanroom._frontend_custom#{}') | ||
|
|
||
| # Base collaboration commands (only list - no --collaboration-id needed) | ||
| with loader.command_group('managedcleanroom frontend collaboration', custom_command_type=frontend_custom) as g: | ||
| g.custom_command('list', 'frontend_collaboration_list') | ||
|
|
||
| # Show command at frontend level (requires --collaboration-id) | ||
| with loader.command_group('managedcleanroom frontend', custom_command_type=frontend_custom) as g: | ||
| g.custom_show_command('show', 'frontend_collaboration_show') | ||
|
|
||
| # Workloads commands | ||
| with loader.command_group('managedcleanroom frontend workloads', custom_command_type=frontend_custom) as g: | ||
|
sakshamgargMS marked this conversation as resolved.
|
||
| g.custom_command('list', 'frontend_collaboration_workloads_list') | ||
|
|
||
| # Analytics commands | ||
| 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( | ||
| 'deploymentinfo', | ||
| 'frontend_collaboration_analytics_deploymentinfo') | ||
| g.custom_command( | ||
| 'cleanroompolicy', | ||
| 'frontend_collaboration_analytics_cleanroompolicy') | ||
|
|
||
| # OIDC commands | ||
| with loader.command_group('managedcleanroom frontend oidc issuerinfo', custom_command_type=frontend_custom) as g: | ||
| g.custom_show_command( | ||
| 'show', 'frontend_collaboration_oidc_issuerinfo_show') | ||
|
|
||
| # Invitation commands | ||
| with loader.command_group('managedcleanroom frontend invitation', custom_command_type=frontend_custom) as g: | ||
| g.custom_command('list', 'frontend_collaboration_invitation_list') | ||
| g.custom_show_command('show', 'frontend_collaboration_invitation_show') | ||
| g.custom_command('accept', 'frontend_collaboration_invitation_accept') | ||
|
|
||
| # Dataset commands | ||
| with loader.command_group('managedcleanroom frontend analytics dataset', custom_command_type=frontend_custom) as g: | ||
| g.custom_command('list', 'frontend_collaboration_dataset_list') | ||
| g.custom_show_command('show', 'frontend_collaboration_dataset_show') | ||
| g.custom_command('publish', 'frontend_collaboration_dataset_publish') | ||
|
|
||
| # Consent commands | ||
| with loader.command_group('managedcleanroom frontend consent', custom_command_type=frontend_custom) as g: | ||
| g.custom_command('check', 'frontend_collaboration_consent_check') | ||
| g.custom_command('set', 'frontend_collaboration_consent_set') | ||
|
|
||
| # Query commands | ||
| with loader.command_group('managedcleanroom frontend analytics query', custom_command_type=frontend_custom) as g: | ||
| g.custom_command('list', 'frontend_collaboration_query_list') | ||
| g.custom_show_command('show', 'frontend_collaboration_query_show') | ||
| g.custom_command('publish', 'frontend_collaboration_query_publish') | ||
| g.custom_command('run', 'frontend_collaboration_query_run') | ||
|
|
||
| # Query vote commands | ||
| with loader.command_group( | ||
| 'managedcleanroom frontend analytics query vote', | ||
| custom_command_type=frontend_custom) as g: | ||
| g.custom_command('accept', 'frontend_collaboration_query_vote_accept') | ||
| g.custom_command('reject', 'frontend_collaboration_query_vote_reject') | ||
|
|
||
| # Query run history commands | ||
| with loader.command_group( | ||
| 'managedcleanroom frontend analytics query runhistory', | ||
| custom_command_type=frontend_custom) as g: | ||
| g.custom_command( | ||
| 'list', 'frontend_collaboration_query_runhistory_list') | ||
|
|
||
| # Query run result commands | ||
| with loader.command_group( | ||
| 'managedcleanroom frontend analytics query runresult', | ||
| custom_command_type=frontend_custom) as g: | ||
| g.custom_show_command( | ||
| 'show', 'frontend_collaboration_query_runresult_show') | ||
|
|
||
| # Audit event commands | ||
| with loader.command_group( | ||
| 'managedcleanroom frontend analytics auditevent', | ||
| custom_command_type=frontend_custom) as g: | ||
| g.custom_command('list', 'frontend_collaboration_audit_list') | ||
|
|
||
| # Attestation commands | ||
| with loader.command_group('managedcleanroom frontend attestation', custom_command_type=frontend_custom) as g: | ||
| g.custom_command('cgs', 'frontend_collaboration_attestation_cgs') | ||
|
|
||
| with loader.command_group( | ||
| 'managedcleanroom frontend analytics attestationreport', | ||
| custom_command_type=frontend_custom) as g: | ||
| g.custom_command( | ||
| 'cleanroom', | ||
| 'frontend_collaboration_attestation_cleanroom') | ||
|
|
||
| # Configuration and authentication commands | ||
| with loader.command_group('managedcleanroom frontend', custom_command_type=frontend_custom) as g: | ||
| g.custom_command('configure', 'frontend_configure') | ||
| g.custom_command('login', 'frontend_login') | ||
| g.custom_command('logout', 'frontend_logout') | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All cmds below this require the --collaboration-id param, should those cmd grps be derived from g then or be sub groups ? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can create everything as a subgroup - but then we will have to build parsing on the commands to know what method to call - right now it structured in a way where all cmdlets call the sdk directly and so this was the simplest way to register the commands without building some parsing logic