Skip to content

Implement subscription filter on az login#33122

Open
xuming-ms wants to merge 1 commit intoAzure:feat/subscription-filteringfrom
xuming-ms:ming/implement-subscription-filter-on-az-login
Open

Implement subscription filter on az login#33122
xuming-ms wants to merge 1 commit intoAzure:feat/subscription-filteringfrom
xuming-ms:ming/implement-subscription-filter-on-az-login

Conversation

@xuming-ms
Copy link
Copy Markdown
Contributor

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.

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

Hi @xuming-ms,
Usually we only allow pull requests to be submitted to the dev branch, please double check your pull request target branch feat/subscription-filtering.

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

azure-client-tools-bot-prd bot commented Apr 1, 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 1, 2026

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

@microsoft-github-policy-service microsoft-github-policy-service bot added the Auto-Assign Auto assign by bot label Apr 2, 2026
@microsoft-github-policy-service microsoft-github-policy-service bot added Account az login/account act-identity-squad Graph (doesn't work with label-triggered comments; use Graph.Microsoft instead) az ad labels Apr 2, 2026
@xuming-ms xuming-ms force-pushed the ming/implement-subscription-filter-on-az-login branch from 9666e63 to 4c236dd Compare April 2, 2026 00:23
@yonzhan
Copy link
Copy Markdown
Collaborator

yonzhan commented Apr 2, 2026

az login

Copy link
Copy Markdown
Member

@isra-fel isra-fel left a comment

Choose a reason for hiding this comment

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

Please check the inline comments. Love the decent test coverage!



# pylint: disable=too-many-branches, too-many-locals
def login(cmd, username=None, password=None, tenant=None, scopes=None, allow_no_subscriptions=False,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would allow_no_subscriptions block -skip-subscription-discovery (bare mode)?
I think the answer is no as it's covered by test_skip_discovery_bare_mode

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct, allow_no_subscriptions does not block --skip-subscription-discovery (bare mode).

set_login_experience_v2(login_experience_v2)

select_subscription = interactive and sys.stdin.isatty() and sys.stdout.isatty() and login_experience_v2
# When --subscription or --skip-subscription-discovery is provided, bypass the interactive selector
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's an edge case: Customers can delegate subscriptions and resource groups to specified users and roles in the managing tenant
meaning the same subscription can appear in multiple tenants, and we might still need to display the selector.

I think a good criteria could be: after we apply all the filters, if there are still multiple subscription candidates, then we display the selector.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point — thanks for calling that out. To address this, I'm
thinking about consolidating the default subscription selection
and the interactive selector into the same place. Would it be
better to move the default selection logic out of _profile.login
and colocate it with the existing interactive selector (I prefer this since this is currently where default is set and reduce the chance to breaking existing code), or to
move the interactive selector into _profile.login instead?

@@ -60,6 +60,18 @@ def load_arguments(self, command):
"WWW-Authenticate header.")
c.ignore('_subscription') # hide the global subscription parameter
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if it makes sense to enable this global parameter.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe we have to ignore this global parameter to accomodate for the new one that we will create in this command.

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

Adds support for filtering/bypassing subscription discovery in az login to improve login performance for tenants with very large numbers of subscriptions, including a “bare” tenant-level login mode and a fast path that retrieves a single subscription by ID.

Changes:

  • Wire --skip-subscription-discovery and --subscription through the profile command module to core Profile.login.
  • Implement core login behavior for (1) skip discovery + specific subscription, (2) skip discovery bare tenant-level account, and (3) full discovery with a chosen default subscription.
  • Add/update help examples and unit tests covering the new login modes and selector bypass.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/azure-cli/azure/cli/command_modules/profile/tests/latest/test_profile_custom.py Adds command-module-level tests for tenant requirement and interactive selector bypass behavior.
src/azure-cli/azure/cli/command_modules/profile/custom.py Passes new parameters into core profile login and bypasses the interactive selector when applicable.
src/azure-cli/azure/cli/command_modules/profile/_help.py Documents new az login usage patterns with examples.
src/azure-cli/azure/cli/command_modules/profile/init.py Registers new az login CLI arguments for skipping discovery and selecting a subscription.
src/azure-cli-core/azure/cli/core/tests/test_profile.py Adds core tests for fast-path GET, bare mode, error/warn behavior, and merge behavior.
src/azure-cli-core/azure/cli/core/_profile.py Implements skip discovery / specific subscription logic and default subscription validation/selection.

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

subscriptions = subscription_finder.find_specific_subscriptions(
tenant, credential, [default_subscription])
elif is_bare_mode:
# Bare mode: no ARM subscription calls. Tenant-level account will be creatd below
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Typo in comment: "creatd" should be "created".

Suggested change
# Bare mode: no ARM subscription calls. Tenant-level account will be creatd below
# Bare mode: no ARM subscription calls. Tenant-level account will be created below

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

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

Copilot AI Apr 2, 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. If tenant is None, the bare-mode branch appends None to subscription_finder.tenants and _build_tenant_level_accounts will be called with [None], and the fast-path branch will call find_specific_subscriptions(None, ...). Add a core-level usage check (similar to command module custom.login) to raise a CLIError when skip_subscription_discovery is set without tenant to keep behavior consistent for programmatic callers.

Copilot uses AI. Check for mistakes.
subscriptions = subscription_finder.find_using_common_tenant(username, credential)

if not subscriptions and not allow_no_subscriptions:
if not subscriptions and not allow_no_subscriptions and not is_bare_mode:
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

When skip_subscription_discovery=True and a default_subscription is provided but cannot be retrieved, find_specific_subscriptions returns an empty list and this code raises CLIError("No subscriptions found for <username>."). This is misleading because the user asked for a specific subscription. Consider special-casing the fast-path to raise an error that mentions the requested subscription ID (and ideally distinguishes not-found/unauthorized vs transient errors), rather than the generic "No subscriptions found" message.

Suggested change
if not subscriptions and not allow_no_subscriptions and not is_bare_mode:
if not subscriptions and not allow_no_subscriptions and not is_bare_mode:
if skip_subscription_discovery and default_subscription:
# Fast-path error: a specific subscription was requested but could not be retrieved
raise CLIError(
"The subscription '{}' could not be retrieved for '{}'. "
"Ensure the subscription exists and that you have access to it.".format(
default_subscription, username))

Copilot uses AI. Check for mistakes.
Comment on lines +229 to +235
cmd = mock.MagicMock()
cmd.cli_ctx = DummyCli()
cmd.cli_ctx.config = mock.MagicMock()
cmd.cli_ctx.config.getboolean.return_value = True # login_experience_v2 = True

result = login(cmd, tenant=tenant_id, skip_subscription_discovery=True)

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

These selector-bypass tests depend on sys.stdin.isatty()/sys.stdout.isatty() being true for the selector to be eligible. In many test runners those are false (captured IO), which would make this test pass even if the bypass logic regresses. Patch sys.stdin.isatty and sys.stdout.isatty to return True so the test actually exercises the selector-bypass condition.

Copilot uses AI. Check for mistakes.
Comment on lines +257 to +266
cmd = mock.MagicMock()
cmd.cli_ctx = DummyCli()
cmd.cli_ctx.config = mock.MagicMock()
cmd.cli_ctx.config.getboolean.return_value = False

# Interactive login (no username) with --subscription
result = login(cmd, tenant='tenant1', default_subscription=sub_id)

# Assert selector was never instantiated
selector_mock.assert_not_called()
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

cmd.cli_ctx.config.getboolean.return_value is set to False here, which disables the interactive selector regardless of --subscription. This makes the test unable to verify that --subscription is what bypasses the selector. Set login_experience_v2 to True (and patch isatty() to True) so the selector would run without --subscription, then assert it is bypassed when default_subscription is provided.

Copilot uses AI. Check for mistakes.
raise CLIError("usage error: '--use-sn-issuer' is only applicable with a service principal")
if service_principal and not username:
raise CLIError('usage error: --service-principal --username NAME --password SECRET --tenant TENANT')
if skip_subscription_discovery and not tenant:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There should be another check for subscription here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could you please clarify what check to add?

@notyashhh notyashhh self-assigned this Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Account az login/account act-identity-squad Auto-Assign Auto assign by bot Graph (doesn't work with label-triggered comments; use Graph.Microsoft instead) az ad

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants