Skip to content

{Profile} Login experience v2#28910

Merged
jiasli merged 1 commit intoAzure:devfrom
jiasli:login
May 15, 2024
Merged

{Profile} Login experience v2#28910
jiasli merged 1 commit intoAzure:devfrom
jiasli:login

Conversation

@jiasli
Copy link
Member

@jiasli jiasli commented May 7, 2024

Related command
az login

Description
Interactive commands now prompt the user to interactively select the default subscription and tenant:

  • az login: Auth code flow/WAM
  • az login --use-device-code: Device code flow

Automation commands are not affected:

  • az login --username --password: Username password flow
  • az login --service-principal: Service principal authentication
  • az login --identity: managed identity authentication

Testing Guide
az login
az login --use-device-code

Turn off:

az config set core.login_experience_v2=off
az login

Additional Information
Require #28660

History Notes

[Profile] az login: Introduce login experience v2. For more details, see https://go.microsoft.com/fwlink/?linkid=2271236
[Core] Add tenantDefaultDomain and tenantDisplayName properties to login contexts (shown by az account list)

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

azure-client-tools-bot-prd bot commented May 7, 2024

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

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

Hi @jiasli,
Since the current milestone time is less than 7 days, this pr will be reviewed in the next milestone.

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

azure-client-tools-bot-prd bot commented May 7, 2024

️✔️AzureCLI-BreakingChangeTest
️✔️Non Breaking Changes

@yonzhan
Copy link
Collaborator

yonzhan commented May 7, 2024

Login experience v2

Comment on lines +594 to +597
except NoTTYException:
# This is a good example showing interactive and non-TTY are not contradictory
logger.warning("No TTY to select the default subscription.")
return active_one
Copy link
Member Author

@jiasli jiasli May 9, 2024

Choose a reason for hiding this comment

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

If no TTY is available, should we:

  1. print the subscription table
  2. echo the selection
  3. simply skip the whole subscription selection process

Copy link
Contributor

Choose a reason for hiding this comment

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

We probably want to skip the selection process in the case there is no TTY available.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, but should we still show the interactive interface, or switch to the previous behavior of returning a JSON?

Copy link
Member Author

Choose a reason for hiding this comment

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

If stdin is not a TTY, the user may be running echo 1 | az login. This is unlikely as the user can't know beforehand which subscription will be the one they want to select, and the number can change.

If stdout is not a TTY, the user may be running az login | grep. Not showing the JSON output will be a breaking change in this case.

Therefore, we decide to show interactive subscription selection only when both stdin and stdout are TTY.

logger = get_logger(__name__)


class SubscriptionSelector:
Copy link
Member Author

Choose a reason for hiding this comment

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

Some refactors are made in commit 5884ecb:

  1. Part of Profile._interactively_select_subscription is split into _format_subscription_table so that it can be unit-tested.
  2. Profile._interactively_select_subscription and _format_subscription_table are extracted into a SubscriptionSelector class.
  3. SubscriptionSelector is moved to profile module to reduce the change on core.

from ._subscription_selector import SubscriptionSelector
from azure.cli.core._profile import _SUBSCRIPTION_ID

selected = SubscriptionSelector(subscriptions)()
Copy link
Member Author

@jiasli jiasli May 10, 2024

Choose a reason for hiding this comment

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

subscriptions here only contain subscriptions accessible by this user, provided to az login. az login's original behavior is also to return subscriptions accessible by this user. Should we also show subscriptions from other users as candidates in interactive selection?

Copy link
Contributor

Choose a reason for hiding this comment

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

If the user does not have access to the subscription, why should be show it?

Copy link
Member Author

@jiasli jiasli May 13, 2024

Choose a reason for hiding this comment

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

Even though the current user doesn't have access, another user used in a previous az login has. Running az account set will automatically switch to the other user.

# create=True is required as running this test alone won't set format_styled_text.theme. patch.object
# will fail because of the missing theme attribute. For example, this command fails:
# azdev test test_format_subscription_table
with mock.patch.object(format_styled_text, 'theme', 'dark', create=True):
Copy link
Member Author

Choose a reason for hiding this comment

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

This mock can also be achieved by deleting the theme attribute, just like running test_format_subscription_table alone. Pytest's monkeypatch fixture has a monkeypatch.delattr(obj, name, raising=True) method for this purpose, but I can't find a way to do this with unittest.mock.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can write some custom logic to achieve this:

        is_attr_set = False
        original_attr_value = None
        if hasattr(format_styled_text, 'theme'):
            is_attr_set = True
            original_attr_value = format_styled_text.theme
            delattr(format_styled_text, 'theme')

        ...

        if is_attr_set:
            format_styled_text.theme = original_attr_value

or even wrap it as a context manager:

class delattr_patch:
    def __init__(self, object, attr):
        self.object = object
        self.attr = attr
        self.is_attr_set = False
        self.original_attr_value = None

    def __enter__(self):
        if hasattr(self.object, self.attr):
            self.is_attr_set = True
            self.original_attr_value = getattr(self.object, self.attr)
            delattr(self.object, self.attr)

    def __exit__(self, exc_type, exc_value, traceback):
        if self.is_attr_set:
            setattr(self.object, self.attr, self.original_attr_value)

Then we can use it as

        with delattr_patch(format_styled_text, 'theme'):
            ...

subscription_name = subscription_name[:subscription_name_length_limit - 3] + '...'

row = {
'No': f'[{index_str}]' + (' ' + self.DEFAULT_ROW_MARKER if is_default else ''),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why No is not highlighted?

Copy link
Member Author

Choose a reason for hiding this comment

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

UX's design.

Copy link
Member Author

Choose a reason for hiding this comment

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

No is also highlighted in the latest code.

password = ServicePrincipalAuth.build_credential(password, client_assertion, use_cert_sn_issuer)

select_subscription = interactive and sys.stdin.isatty() and sys.stdout.isatty() and \
cmd.cli_ctx.config.getboolean('core', 'login_experience_v2', fallback=True)
Copy link
Contributor

@bebound bebound May 13, 2024

Choose a reason for hiding this comment

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

This is long logic, how about creating a function?

If service_principal is not None, the select_subscription is also enabled? This conflicts with PR description.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is long logic, how about creating a function?

True, but not that complex as it is still only one line.

If service_principal is not None, the select_subscription is also enabled? This conflicts with PR description.

service_principal must be used together with username:

if service_principal and not username:
raise CLIError('usage error: --service-principal --username NAME --password SECRET --tenant TENANT')

interactive is True only when username is not provided:

if username:
if not (password or client_assertion):
try:
password = prompt_pass('Password: ')
except NoTTYException:
raise CLIError('Please specify both username and password in non-interactive mode.')
else:
interactive = True

In other words, when username is provided, interactive must be False.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about az login --identity?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is guarded by

if identity:
if in_cloud_console():
return profile.login_in_cloud_shell()
return profile.login_with_managed_identity(username, allow_no_subscriptions)
if in_cloud_console(): # tell users they might not need login
logger.warning(_CLOUD_CONSOLE_LOGIN_WARNING)

Comment on lines +155 to +156
login_experience_v2 = cmd.cli_ctx.config.getboolean('core', 'login_experience_v2', fallback=True)
# Send login_experience_v2 config to telemetry
from azure.cli.core.telemetry import set_login_experience_v2
set_login_experience_v2(login_experience_v2)

select_subscription = interactive and sys.stdin.isatty() and sys.stdout.isatty() and login_experience_v2
Copy link
Member Author

Choose a reason for hiding this comment

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

Should we also send select_subscription to telemetry? login_experience_v2 stands for the config value, while select_subscription stands for whether interactive selection is actually invoked. @dcaro

Copy link
Member Author

Choose a reason for hiding this comment

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

We'll consider adding telemetry for select_subscription in the future.

Comment on lines +76 to +79
# authentication-related
self.enable_broker_on_windows = None
self.msal_telemetry = None
self.login_experience_v2 = None
Copy link
Member Author

@jiasli jiasli May 13, 2024

Choose a reason for hiding this comment

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

Grouping these properties together for better readability.

# authentication-related
set_custom_properties(result, 'EnableBrokerOnWindows', str(self.enable_broker_on_windows))
set_custom_properties(result, 'MsalTelemetry', self.msal_telemetry)
set_custom_properties(result, 'LoginExperienceV2', str(self.login_experience_v2))
Copy link
Member Author

Choose a reason for hiding this comment

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

Should we force str values? self.enable_broker_on_windows is also a bool and we convert it to str (#24304). Can we use bool as it is? @evelyn-ys

Copy link
Member Author

Choose a reason for hiding this comment

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

bool may not appear correctly in the final telemetry. Using str is more robust.

@jiasli
Copy link
Member Author

jiasli commented May 13, 2024

After #26854, there is no easy way to verify the telemetry record sent. I have to add a breakpoint at

logger.info('Begin creating telemetry upload process.')

and check cli_payload contains "Context.Default.AzureCLI.LoginExperienceV2": "True". I think it'll be better to have some other mechanism to monitor the telemetry record sent, such as logging it in the debug log. @evelyn-ys

@jiasli jiasli marked this pull request as ready for review May 14, 2024 10:22
@jiasli jiasli requested a review from necusjz as a code owner May 14, 2024 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Account az login/account Auto-Assign Auto assign by bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants