Skip to content

Implement subscription filter#33181

Open
xuming-ms wants to merge 3 commits intodevfrom
feat/subscription-filtering
Open

Implement subscription filter#33181
xuming-ms wants to merge 3 commits intodevfrom
feat/subscription-filtering

Conversation

@xuming-ms
Copy link
Copy Markdown
Contributor

@xuming-ms xuming-ms commented Apr 14, 2026

Related command
az login

Description
Wires --skip-subscription-discovery and --subscription parameters into az login. This enables fast login for tenants with 1M+ subscriptions by skipping full subscription enumeration.

Three login modes are now supported:

  • --skip-subscription-discovery --subscription S: Fast path: fetches only the specified subscription via GET /subscriptions/{id} (1 API call)
  • --skip-subscription-discovery (bare mode): No ARM calls and creates a tenant-level account for tenant-level operations (e.g., az ad)
  • --subscription S (without skip): Full discovery, then sets S as the default

Both --subscription and --skip-subscription-discovery bypass the interactive subscription selector.

Related issues: #31939, #14933, #13285

Testing Guide
8 unit tests added in TestLoginSubscriptionFilter:

  • Fast path with --skip-subscription-discovery --subscription verifies GET called, not LIST
  • Bare mode verifies 0 ARM calls, tenant-level account created
  • Inaccessible subscription with skip verifies error raised
  • Inaccessible subscription with skip + --allow-no-subscriptions verifies warning, tenant-level fallback
  • --subscription without skip verifies full discovery + default set
  • --subscription not found without skip verifies error
  • Skip with subscription preserves prior subscriptions from other logins
  • Bare mode preserves prior subscriptions from other logins

3 tests added in test_profile_custom.py:
- Add validation that --skip-subscription-discovery requires --tenant

  • --skip-subscription-discovery bypasses interactive selector
  • --subscription bypasses interactive selector

Run tests:

python -m pytest src/azure-cli-core/azure/cli/core/tests/test_profile.py::TestLoginSubscriptionFilter -v
python -m pytest src/azure-cli/azure/cli/command_modules/profile/tests/latest/test_profile_custom.py -v -k "skip_subscription_discovery or default_subscription"

History Notes
[Core] az login: Add --skip-subscription-discovery and --subscription parameters to enable fast login for tenants with many subscriptions


This checklist is used to make sure that common guidelines for a pull request are followed.

Copilot AI review requested due to automatic review settings April 14, 2026 06:31
@azure-client-tools-bot-prd
Copy link
Copy Markdown

azure-client-tools-bot-prd bot commented Apr 14, 2026

️✔️AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.13
️✔️acs
️✔️latest
️✔️3.12
️✔️3.13
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.13
️✔️ams
️✔️latest
️✔️3.12
️✔️3.13
️✔️apim
️✔️latest
️✔️3.12
️✔️3.13
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.13
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️aro
️✔️latest
️✔️3.12
️✔️3.13
️✔️backup
️✔️latest
️✔️3.12
️✔️3.13
️✔️batch
️✔️latest
️✔️3.12
️✔️3.13
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.13
️✔️billing
️✔️latest
️✔️3.12
️✔️3.13
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.13
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.13
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.13
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.13
️✔️config
️✔️latest
️✔️3.12
️✔️3.13
️✔️configure
️✔️latest
️✔️3.12
️✔️3.13
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.13
️✔️container
️✔️latest
️✔️3.12
️✔️3.13
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.13
️✔️core
️✔️latest
️✔️3.12
️✔️3.13
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.13
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.13
️✔️dls
️✔️latest
️✔️3.12
️✔️3.13
️✔️dms
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.13
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.13
️✔️find
️✔️latest
️✔️3.12
️✔️3.13
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.13
️✔️identity
️✔️latest
️✔️3.12
️✔️3.13
️✔️iot
️✔️latest
️✔️3.12
️✔️3.13
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.13
️✔️lab
️✔️latest
️✔️3.12
️✔️3.13
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️maps
️✔️latest
️✔️3.12
️✔️3.13
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.13
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.13
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.13
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.13
️✔️network
️✔️latest
️✔️3.12
️✔️3.13
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.13
️✔️postgresql
️✔️latest
️✔️3.12
️✔️3.13
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.13
️✔️profile
️✔️latest
️✔️3.12
️✔️3.13
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.13
️✔️redis
️✔️latest
️✔️3.12
️✔️3.13
️✔️relay
️✔️latest
️✔️3.12
️✔️3.13
️✔️resource
️✔️latest
️✔️3.12
️✔️3.13
️✔️role
️✔️latest
️✔️3.12
️✔️3.13
️✔️search
️✔️latest
️✔️3.12
️✔️3.13
️✔️security
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.13
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.13
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.13
️✔️sql
️✔️latest
️✔️3.12
️✔️3.13
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.13
️✔️storage
️✔️latest
️✔️3.12
️✔️3.13
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.13
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.13
️✔️util
️✔️latest
️✔️3.12
️✔️3.13
️✔️vm
️✔️latest
️✔️3.12
️✔️3.13

@azure-client-tools-bot-prd
Copy link
Copy Markdown

azure-client-tools-bot-prd bot commented Apr 14, 2026

⚠️AzureCLI-BreakingChangeTest
⚠️profile
rule cmd_name rule_message suggest_message
⚠️ 1006 - ParaAdd login cmd login added parameter skip_subscription_discovery
⚠️ 1006 - ParaAdd login cmd login added parameter subscription

@yonzhan
Copy link
Copy Markdown
Collaborator

yonzhan commented Apr 14, 2026

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions
Copy link
Copy Markdown

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements subscription filtering controls for az login, allowing callers to skip full subscription discovery and/or set a default subscription during login.

Changes:

  • Adds --skip-subscription-discovery (--skip-sub) and enhances --subscription behavior for az login, including updated help examples.
  • Updates login flow to conditionally bypass or invoke the interactive subscription selector based on filter usage and match count.
  • Extends core profile logic (Profile.login, SubscriptionFinder) to support fast-path subscription fetch (GET) and bare tenant-level login, with new unit and live E2E tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/azure-cli/azure/cli/command_modules/profile/tests/latest/test_subscription_filter_e2e.py Adds live E2E coverage for skip-discovery and subscription filtering login scenarios.
src/azure-cli/azure/cli/command_modules/profile/tests/latest/test_profile_custom.py Adds command-module unit tests for selector behavior and argument validation in custom.login().
src/azure-cli/azure/cli/command_modules/profile/custom.py Implements filter-aware selector behavior and wires new args into Profile.login().
src/azure-cli/azure/cli/command_modules/profile/_help.py Documents new login scenarios in help text.
src/azure-cli/azure/cli/command_modules/profile/init.py Registers new CLI arguments for az login.
src/azure-cli-core/azure/cli/core/tests/test_profile.py Adds core unit tests for skip discovery, fast-path subscription fetch, merge behavior, and preferred subscription selection.
src/azure-cli-core/azure/cli/core/_profile.py Implements skip discovery (fast-path GET / bare mode), subscription filtering, preferred default selection, and specific-subscription finder method.
Comments suppressed due to low confidence (1)

src/azure-cli-core/azure/cli/core/_profile.py:506

  • _set_subscriptions still keys accounts only by subscription ID when secondary_key_name is not provided (_get_key_name), so two accounts with the same subscription ID but different tenantId will overwrite each other during dic.update(...). This conflicts with the new login flow that can surface multiple matches for a single --subscription (e.g., across tenants) and makes it impossible to reliably persist the user's selection. Consider incorporating tenant ID into the key when duplicates exist (or otherwise storing only the selected match) so matches are not silently dropped.
        def _get_key_name(account, secondary_key_name):
            return (account[_SUBSCRIPTION_ID] if secondary_key_name is None
                    else '{}-{}'.format(account[_SUBSCRIPTION_ID], account[secondary_key_name]))


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +128 to +130
selected = SubscriptionSelector(subscriptions)()
profile.set_active_subscription(selected[_SUBSCRIPTION_ID])

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The selector path can present multiple entries that share the same subscription ID (e.g., the same subscription appearing under multiple tenants), but _select_and_set_active ultimately calls profile.set_active_subscription(selected[_SUBSCRIPTION_ID]), which cannot express the chosen tenant and will either be ambiguous (if both entries are stored) or will just select whichever duplicate entry was last persisted (if duplicates were de-duped). To make the interactive selection meaningful, the activation step needs to disambiguate by both subscription ID and tenant (or ensure only one chosen match is persisted before calling set_active_subscription).

Suggested change
selected = SubscriptionSelector(subscriptions)()
profile.set_active_subscription(selected[_SUBSCRIPTION_ID])
def _get_tenant_id(subscription):
return (subscription.get('tenantId') or
subscription.get('tenant_id') or
subscription.get('homeTenantId') or
subscription.get('home_tenant_id'))
selected = SubscriptionSelector(subscriptions)()
selected_subscription_id = selected[_SUBSCRIPTION_ID]
selected_tenant_id = _get_tenant_id(selected)
matching_subscriptions = [
subscription for subscription in subscriptions
if subscription.get(_SUBSCRIPTION_ID) == selected_subscription_id
]
matching_tenant_ids = {_get_tenant_id(subscription) for subscription in matching_subscriptions}
if len(matching_subscriptions) > 1 and len(matching_tenant_ids) > 1:
raise CLIError(
"The selected subscription '{}' is available in multiple tenants{} and cannot be "
"activated unambiguously in interactive mode. Please login again with '--tenant' "
"to select the intended tenant explicitly.".format(
selected_subscription_id,
" (selected tenant: '{}')".format(selected_tenant_id) if selected_tenant_id else ""
)
)
profile.set_active_subscription(selected_subscription_id)

Copilot uses AI. Check for mistakes.
Comment on lines +203 to +213
is_bare_mode = skip_subscription_discovery and not subscription

if skip_subscription_discovery and subscription:
# Fast path: fetch only the specified subscription (1 API call)
subscriptions = subscription_finder.find_specific_subscriptions(
tenant, credential, [subscription])
elif is_bare_mode:
# Bare mode: no ARM subscription calls. Tenant-level account will be created below
subscriptions = []
subscription_finder.tenants.append(tenant)
elif tenant:
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile.login() accepts skip_subscription_discovery=True but doesn't validate that tenant is provided. In the bare-mode branch this appends tenant to subscription_finder.tenants and later _build_tenant_level_accounts will do '/subscriptions/' + t, which will raise a TypeError if tenant is None. Add an explicit early CLIError (similar to profile.custom.login) when skip_subscription_discovery is set without tenant to avoid an unhandled exception and to give a clear message to API callers.

Copilot uses AI. Check for mistakes.
Comment on lines 235 to +253
consolidated = self._normalize_properties(username, subscriptions,
is_service_principal, bool(use_cert_sn_issuer))
self._set_subscriptions(consolidated, preferred_subscription=subscription)

if subscription:
matches = [s for s in consolidated
if s[_SUBSCRIPTION_ID].lower() == subscription.lower() or
s.get(_SUBSCRIPTION_NAME, '').lower() == subscription.lower()]
if matches:
return deepcopy(matches)
if skip_subscription_discovery:
# --skip-subscription-discovery + --subscription S, but S is inaccessible.
# without --allow-no-subscriptions → already errored above
# with --allow-no-subscriptions → tenant-level account only (we reach here)
logger.warning("Subscription '%s' not found. Profile has tenant-level account only.",
subscription)
else:
raise CLIError("Subscription '{}' not found. Check the ID or name and try again."
.format(subscription))
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile.login() persists subscriptions via _set_subscriptions(...) before validating --subscription matches anything. In the subscription-provided + not-skip_subscription_discovery case, this means a subsequent CLIError("Subscription ... not found") can still leave the user logged in and mutate azureProfile.json/default subscription even though the command failed. Consider validating/filtering the match first and only calling _set_subscriptions after you know the requested subscription is valid (or rolling back on error).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants