| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/signin/dice_web_signin_interceptor.h" |
| |
| #include <optional> |
| #include <string> |
| |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/hash/hash.h" |
| #include "base/i18n/case_conversion.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/enterprise/browser_management/management_service_factory.h" |
| #include "chrome/browser/enterprise/util/managed_browser_utils.h" |
| #include "chrome/browser/net/system_network_context_manager.h" |
| #include "chrome/browser/new_tab_page/chrome_colors/generated_colors_info.h" |
| #include "chrome/browser/password_manager/chrome_password_manager_client.h" |
| #include "chrome/browser/policy/chrome_browser_policy_connector.h" |
| #include "chrome/browser/policy/cloud/user_policy_signin_service_factory.h" |
| #include "chrome/browser/policy/profile_policy_connector.h" |
| #include "chrome/browser/profiles/profile_attributes_entry.h" |
| #include "chrome/browser/profiles/profile_attributes_storage.h" |
| #include "chrome/browser/profiles/profile_avatar_icon_util.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/profiles/profile_metrics.h" |
| #include "chrome/browser/profiles/profiles_state.h" |
| #include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h" |
| #include "chrome/browser/search_engines/template_url_service_factory.h" |
| #include "chrome/browser/signin/chrome_signin_pref_names.h" |
| #include "chrome/browser/signin/dice_intercepted_session_startup_helper.h" |
| #include "chrome/browser/signin/dice_signed_in_profile_creator.h" |
| #include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/browser/signin/signin_util.h" |
| #include "chrome/browser/signin/web_signin_interceptor.h" |
| #include "chrome/browser/themes/theme_service.h" |
| #include "chrome/browser/themes/theme_service_factory.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h" |
| #include "chrome/browser/ui/profiles/profile_colors_util.h" |
| #include "chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/common/channel_info.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/themes/autogenerated_theme_util.h" |
| #include "components/password_manager/core/browser/password_manager.h" |
| #include "components/password_manager/core/common/password_manager_ui.h" |
| #include "components/policy/core/browser/signin/profile_separation_policies.h" |
| #include "components/policy/core/browser/signin/user_cloud_signin_restriction_policy_fetcher.h" |
| #include "components/policy/core/common/policy_map.h" |
| #include "components/policy/core/common/policy_namespace.h" |
| #include "components/policy/core/common/policy_service.h" |
| #include "components/policy/core/common/policy_utils.h" |
| #include "components/policy/policy_constants.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/search_engines/search_engine_choice/search_engine_choice_utils.h" |
| #include "components/search_engines/template_url_data.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "components/signin/public/base/consent_level.h" |
| #include "components/signin/public/base/signin_metrics.h" |
| #include "components/signin/public/base/signin_pref_names.h" |
| #include "components/signin/public/base/signin_prefs.h" |
| #include "components/signin/public/base/signin_switches.h" |
| #include "components/signin/public/identity_manager/account_capabilities.h" |
| #include "components/signin/public/identity_manager/account_info.h" |
| #include "components/signin/public/identity_manager/account_managed_status_finder.h" |
| #include "components/signin/public/identity_manager/accounts_mutator.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/signin/public/identity_manager/primary_account_mutator.h" |
| #include "components/signin/public/identity_manager/tribool.h" |
| #include "google_apis/gaia/gaia_auth_util.h" |
| #include "google_apis/gaia/gaia_id.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/mojom/themes.mojom.h" |
| |
| namespace { |
| |
| constexpr char kChromeSingInInterceptionSupervisionStateHistogramPrefix[] = |
| "Signin.Intercept.Heuristic.SupervisionState"; |
| |
| constexpr size_t kMaxChromeSigninInterceptionDismissCount = 5; |
| |
| // The user will only see the Chrome Signin bubble reprompt a maximum of 4 times |
| // (not including the initial time the bubble was declined). |
| static constexpr int kMaxChromeSigninBubbleRepromptCountAllowed = 4; |
| // The Chrome Signin bubble can be reprompted only if a minimum duration time |
| // has passed since the last bubble was shown (either initial bubble or a |
| // reprompt). |
| static constexpr base::TimeDelta kMinimumDurationForChromeSigninBubbleReprompt = |
| base::Days(60); |
| |
| // Helper function to return the primary account info. The returned info is |
| // empty if there is no primary account, and non-empty otherwise. Extended |
| // fields may be missing if they are not available. |
| AccountInfo GetPrimaryAccountInfo(signin::IdentityManager* manager) { |
| CoreAccountInfo primary_core_account_info = |
| manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); |
| if (primary_core_account_info.IsEmpty()) { |
| return AccountInfo(); |
| } |
| |
| AccountInfo primary_account_info = |
| manager->FindExtendedAccountInfo(primary_core_account_info); |
| |
| if (!primary_account_info.IsEmpty()) { |
| return primary_account_info; |
| } |
| |
| // Return an AccountInfo without extended fields, based on the core info. |
| AccountInfo account_info; |
| account_info.gaia = primary_core_account_info.gaia; |
| account_info.email = primary_core_account_info.email; |
| account_info.account_id = primary_core_account_info.account_id; |
| return account_info; |
| } |
| |
| bool IsFirstAccount(signin::IdentityManager* manager, |
| const std::string& email) { |
| std::vector<CoreAccountInfo> accounts_in_chrome = |
| manager->GetAccountsWithRefreshTokens(); |
| // There is not guarantee that the added email/account have a refresh token |
| // yet. |
| // So we either check that no account exists, or that the only account that |
| // has a refresh token is the account we are interested in, to make sure it is |
| // the first account. |
| return accounts_in_chrome.size() == 0 || |
| (accounts_in_chrome.size() == 1 && |
| gaia::AreEmailsSame(email, accounts_in_chrome[0].email)); |
| } |
| |
| // Returns the time elapsed since the last Chrome Signin Bubble decline. It is |
| // expected that a first decline was made and that the setting was not manually |
| // overridden by the user to do not sign in automatically. In case the |
| // conditions are not met to retrieve a proper delta time, a default value of 0 |
| // is returned. |
| base::TimeDelta GetTimeSinceLastChromeSigninDecline( |
| const SigninPrefs& signin_prefs, |
| const GaiaId& gaia_id) { |
| std::optional<base::Time> last_bubble_decline_time = |
| signin_prefs.GetChromeSigninInterceptionLastBubbleDeclineTime(gaia_id); |
| // If the value does not exist, this means that the user is either not in the |
| // `ChromeSigninUserChoice::kDoNotSignin` or they explicitly changed the |
| // setting from the settings page. |
| if (!last_bubble_decline_time.has_value()) { |
| return base::TimeDelta(); |
| } |
| |
| return base::Time::Now() - last_bubble_decline_time.value(); |
| } |
| |
| // When a user declines the bubble, potential reprompts are allowed in future |
| // based on the time that passes since the bubble decline and the last |
| // reprompts, with a fixed amount of allowed reprompts per account. |
| // Reprompts are not allowed if the user explicitly set the Chrome Signin |
| // setting to do not signin. |
| bool ShouldAllowChromeSigninBubbleReprompt(const SigninPrefs& signin_prefs, |
| const GaiaId& gaia_id) { |
| // Reprompts are only allowed if the user choice is to not sign in |
| // automatically. |
| if (signin_prefs.GetChromeSigninInterceptionUserChoice(gaia_id) != |
| ChromeSigninUserChoice::kDoNotSignin) { |
| return false; |
| } |
| |
| // Maximum reprompt count check. |
| int reprompt_count = signin_prefs.GetChromeSigninBubbleRepromptCount(gaia_id); |
| if (reprompt_count >= kMaxChromeSigninBubbleRepromptCountAllowed) { |
| return false; |
| } |
| |
| // If the user has set the setting manually, this value will be 0, making sure |
| // it does not satisfy the minimum requirement for repromts. |
| base::TimeDelta time_since_last_decline = |
| GetTimeSinceLastChromeSigninDecline(signin_prefs, gaia_id); |
| |
| // Minimum duration since last chrome signin decline check. |
| return time_since_last_decline >= |
| kMinimumDurationForChromeSigninBubbleReprompt; |
| } |
| |
| // Set showing the bubble as a reprompt if the user previously declined the |
| // bubble and did not override the choice in the settings page (having a |
| // declined choice time). |
| void MaybeUpdateRepromptInfoAfterDecline(SigninPrefs& signin_prefs, |
| const GaiaId& gaia_id) { |
| // Check if this is was a reprompt. |
| if (!ShouldAllowChromeSigninBubbleReprompt(signin_prefs, gaia_id)) { |
| return; |
| } |
| |
| // Record this value before updating the new decline time. |
| // There is no need to record values with less than the minimum duration, and |
| // recording max values with up to 1 year (365 days). There is no need for |
| // more granularity. |
| base::UmaHistogramCustomCounts( |
| "Signin.Intercept.ChromeSignin.NumberOfDaysSinceLastDecline", |
| GetTimeSinceLastChromeSigninDecline(signin_prefs, gaia_id).InDays(), |
| /*min=*/kMinimumDurationForChromeSigninBubbleReprompt.InDays(), |
| /*exclusive_max=*/365 /*days*/, |
| /*buckets=*/50); |
| |
| signin_prefs.SetChromeSigninInterceptionLastBubbleDeclineTime( |
| gaia_id, base::Time::Now()); |
| int new_reprompt_count = |
| signin_prefs.IncrementChromeSigninBubbleRepromptCount(gaia_id); |
| |
| base::UmaHistogramExactLinear("Signin.Intercept.ChromeSignin.RepromptCount", |
| new_reprompt_count, |
| kMaxChromeSigninBubbleRepromptCountAllowed + 1); |
| } |
| |
| // Returns `ShouldShowChromeSigninBubbleWithReason::kShouldShow` if sign in |
| // happens through the web and Chrome isn't already signed in. |
| ShouldShowChromeSigninBubbleWithReason MaybeShouldShowChromeSigninBubble( |
| PrefService& pref_service, |
| signin::IdentityManager* manager, |
| const GaiaId& gaia_id, |
| signin_metrics::AccessPoint access_point) { |
| // If the access point is not set, we cannot accurately know if we have to |
| // show the bubble or not, so we will not show it. |
| if (access_point == signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN) { |
| return ShouldShowChromeSigninBubbleWithReason:: |
| kShouldNotShowUnknownAccessPoint; |
| } |
| |
| // Only show the Chrome Signin Bubble when the signin event occurred through |
| // a regular web signin in (not triggered through a chrome feature). |
| if (access_point != signin_metrics::AccessPoint::ACCESS_POINT_WEB_SIGNIN) { |
| return ShouldShowChromeSigninBubbleWithReason:: |
| kShouldNotShowNotFromWebSignin; |
| } |
| |
| // Check if an account is already signed in to Chrome. |
| // |
| // If explicit browser signin is disabled, we ignore this condition since the |
| // primary account will be set prior to this call. This is done for metric |
| // purposes, this is safe since the bubble will not be shown in that case any |
| // way. |
| if (switches::IsExplicitBrowserSigninUIOnDesktopEnabled() && |
| manager->HasPrimaryAccount(signin::ConsentLevel::kSignin)) { |
| return ShouldShowChromeSigninBubbleWithReason:: |
| kShouldNotShowAlreadySignedIn; |
| } |
| |
| // Check for the Chrome Signin setting value and possible reprompts. |
| if (switches::IsExplicitBrowserSigninUIOnDesktopEnabled()) { |
| SigninPrefs signin_prefs(pref_service); |
| ChromeSigninUserChoice user_choice = |
| signin_prefs.GetChromeSigninInterceptionUserChoice(gaia_id); |
| switch (user_choice) { |
| case ChromeSigninUserChoice::kNoChoice: |
| case ChromeSigninUserChoice::kAlwaysAsk: |
| break; |
| case ChromeSigninUserChoice::kSignin: |
| // This should not happen in a regular case, but rather an edge case; if |
| // the user changed their preference while the interception is in |
| // progress. Might also happen during tests that do not test the full |
| // flow; mainly the early flow that automatically signs in and do not |
| // get to this point. |
| return ShouldShowChromeSigninBubbleWithReason::kShouldNotShowUserChoice; |
| case ChromeSigninUserChoice::kDoNotSignin: |
| if (!ShouldAllowChromeSigninBubbleReprompt(signin_prefs, gaia_id)) { |
| return ShouldShowChromeSigninBubbleWithReason:: |
| kShouldNotShowUserChoice; |
| } |
| break; |
| } |
| } |
| |
| return ShouldShowChromeSigninBubbleWithReason::kShouldShow; |
| } |
| |
| // Returns true if we have the minimum extended account information needed to |
| // make a best-effort intercept heuristic decision. If we fail to retrieve |
| // this information we will cancel the interception completely. |
| // Returns false otherwise. |
| bool IsRequiredExtendedAccountInfoAvailable(const AccountInfo& account_info) { |
| return account_info.IsValid(); |
| } |
| |
| // Returns true if enterprise separation is required. |
| // Returns false is enterprise separation is not required. |
| // Returns no value if info is required to determine if enterprise separation |
| // is required. If `profile_separation_policies` is `std::nullopt` then the |
| // user cloud profile separation policies have not yet been fetched. |
| std::optional<bool> EnterpriseSeparationMaybeRequired( |
| Profile* profile, |
| signin::IdentityManager* identity_manager, |
| const std::string& email, |
| bool is_new_account_interception, |
| const std::optional<policy::ProfileSeparationPolicies>& |
| intercepted_profile_separation_policies, |
| bool expects_intercepted_profile_separation_policies_for_testing) { |
| CoreAccountInfo primary_core_account_info = |
| identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); |
| |
| // Enforce separation for new accounts or re-auth of existing secondary |
| // accounts. |
| if ((is_new_account_interception || |
| !gaia::AreEmailsSame(primary_core_account_info.email, email)) && |
| !signin_util::IsAccountExemptedFromEnterpriseProfileSeparation(profile, |
| email)) { |
| return true; |
| } |
| |
| // No enterprise separation required for consumer accounts. |
| if (!signin::AccountManagedStatusFinder::MayBeEnterpriseUserBasedOnEmail( |
| email)) { |
| return false; |
| } |
| |
| auto intercepted_account_info = |
| identity_manager->FindExtendedAccountInfoByEmailAddress(email); |
| // If the account info is not found, we need to wait for the info to be |
| // available. |
| if (!IsRequiredExtendedAccountInfoAvailable(intercepted_account_info)) { |
| return std::nullopt; |
| } |
| // If the intercepted account is not managed, no interception required. |
| if (!intercepted_account_info.IsManaged()) { |
| return false; |
| } |
| // If `profile` requires enterprise profile separation, return true. |
| // Here we only check the legacy policy by passing an empty email since the |
| // new ProfileSeparationSetting policy is checked early in the function. |
| if (signin_util::IsProfileSeparationEnforcedByProfile( |
| profile, |
| /*intercepted_account_email=*/std::string())) { |
| return true; |
| } |
| |
| if (signin_util::IsProfileSeparationEnforcedByPolicies( |
| intercepted_profile_separation_policies.value_or( |
| policy::ProfileSeparationPolicies()))) { |
| return true; |
| } |
| |
| // If we still do not know if profile separation is required, the account |
| // level policies for the intercepted account must be fetched if possible. |
| // If `g_browser_process->system_network_context_manager()` is equal to |
| // nullptr, we are probably in tests and should not try to fetch any policies. |
| // If `expects_intercepted_profile_separation_policies_for_testing`, even if |
| // we are in tests, we have a value set locally that will not require us to |
| // make a network call. |
| // Fetching the value will not be possible isf we cannot make network calls |
| // nor have a value set locally for testing. |
| if (is_new_account_interception && |
| !intercepted_profile_separation_policies.has_value() && |
| (g_browser_process->system_network_context_manager() || |
| expects_intercepted_profile_separation_policies_for_testing)) { |
| return std::nullopt; |
| } |
| |
| return false; |
| } |
| |
| void RecordShouldShowChromeSigninBubbleReason( |
| ShouldShowChromeSigninBubbleWithReason reason) { |
| // This metric will be recorded both when |
| // `switches::kExplicitBrowserSigninUIOnDesktop` is enabled and disabled when |
| // the Chrome Signin bubble is expected to be shown or not. |
| base::UmaHistogramEnumeration( |
| "Signin.Intercept.Heuristic.ShouldShowChromeSigninBubbleWithReason", |
| reason); |
| } |
| |
| bool IsPrimaryAccountInterception(const CoreAccountId& account_id, |
| signin::IdentityManager* identity_manager) { |
| return account_id == |
| identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin); |
| } |
| |
| bool IsReauthPrimaryAccount(bool new_account_interception, |
| const CoreAccountId& account_id, |
| signin::IdentityManager* identity_manager) { |
| return !new_account_interception && |
| IsPrimaryAccountInterception(account_id, identity_manager); |
| } |
| |
| // Returns true if the current state is inconsistent, which happens by |
| // signing into the web with an account B while being in sign in pending state |
| // with account A. Returns false otherwise. Used for metrics. |
| bool IsInInconsistentStateWithPrimaryAccount( |
| const CoreAccountId& account_id, |
| signin::IdentityManager* identity_manager) { |
| return signin_util::IsSigninPending(identity_manager) && |
| identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin) |
| .account_id != account_id; |
| } |
| |
| SinginInterceptSupervisionState CapabilityToSupervisionState( |
| const AccountCapabilities& capabilities) { |
| const signin::Tribool is_supervised = |
| capabilities.is_subject_to_parental_controls(); |
| switch (is_supervised) { |
| case (signin::Tribool::kTrue): |
| return SinginInterceptSupervisionState::kSupervisedUser; |
| case (signin::Tribool::kFalse): |
| return SinginInterceptSupervisionState::kRegularUser; |
| case (signin::Tribool::kUnknown): { |
| return SinginInterceptSupervisionState::kUnknownSupervision; |
| } |
| } |
| NOTREACHED(); |
| } |
| |
| void MaybeRecordSupervisedUserStateMetrics( |
| const AccountInfo& intercepted_account_info, |
| WebSigninInterceptor::SigninInterceptionType interception_type) { |
| if (interception_type != |
| WebSigninInterceptor::SigninInterceptionType::kChromeSignin && |
| interception_type != |
| WebSigninInterceptor::SigninInterceptionType::kMultiUser && |
| interception_type != |
| WebSigninInterceptor::SigninInterceptionType::kProfileSwitch) { |
| return; |
| } |
| |
| base::UmaHistogramEnumeration( |
| kChromeSingInInterceptionSupervisionStateHistogramPrefix + |
| DiceWebSigninInterceptorDelegate::GetHistogramSuffix( |
| interception_type), |
| CapabilityToSupervisionState(intercepted_account_info.capabilities)); |
| } |
| |
| } // namespace |
| |
| DiceWebSigninInterceptor::DiceWebSigninInterceptor( |
| Profile* profile, |
| std::unique_ptr<WebSigninInterceptor::Delegate> delegate) |
| : profile_(profile), |
| identity_manager_(IdentityManagerFactory::GetForProfile(profile)), |
| delegate_(std::move(delegate)), |
| state_(std::make_unique<ResetableState>()) { |
| DCHECK(profile_); |
| DCHECK(identity_manager_); |
| DCHECK(delegate_); |
| } |
| |
| DiceWebSigninInterceptor::~DiceWebSigninInterceptor() = default; |
| |
| DiceWebSigninInterceptor::ResetableState::ResetableState() = default; |
| DiceWebSigninInterceptor::ResetableState::~ResetableState() = default; |
| |
| DiceWebSigninInterceptor::ProfilePresets::ProfilePresets(SkColor profile_color) |
| : profile_color(profile_color) {} |
| |
| DiceWebSigninInterceptor::ProfilePresets::~ProfilePresets() = default; |
| |
| // static |
| void DiceWebSigninInterceptor::RegisterProfilePrefs( |
| user_prefs::PrefRegistrySyncable* registry) { |
| registry->RegisterDictionaryPref(prefs::kProfileCreationInterceptionDeclined); |
| registry->RegisterBooleanPref(prefs::kSigninInterceptionEnabled, true); |
| registry->RegisterStringPref(prefs::kManagedAccountsSigninRestriction, |
| std::string()); |
| registry->RegisterStringPref(prefs::kSigninInterceptionIDPCookiesUrl, |
| std::string()); |
| registry->RegisterBooleanPref( |
| prefs::kManagedAccountsSigninRestrictionScopeMachine, false); |
| registry->RegisterIntegerPref(prefs::kProfileSeparationSettings, 0); |
| registry->RegisterIntegerPref(prefs::kProfileSeparationDataMigrationSettings, |
| 1); |
| registry->RegisterListPref(prefs::kProfileSeparationDomainExceptionList); |
| registry->RegisterStringPref( |
| prefs::kUserCloudSigninPolicyResponseFromPolicyTestPage, std::string()); |
| } |
| |
| std::optional<SigninInterceptionHeuristicOutcome> |
| DiceWebSigninInterceptor::GetHeuristicOutcome( |
| bool is_new_account, |
| bool is_sync_signin, |
| const std::string& email, |
| const GaiaId& gaia_id, |
| bool update_state, |
| const ProfileAttributesEntry** entry) const { |
| bool signin_interception_enabled = |
| profile_->GetPrefs()->GetBoolean(prefs::kSigninInterceptionEnabled); |
| |
| if (is_sync_signin) { |
| // Do not intercept signins from the Sync startup flow. |
| // Note: |is_sync_signin| is an approximation, and in rare cases it may be |
| // true when in fact the signin was not a sync signin. In this case the |
| // interception is missed. |
| return SigninInterceptionHeuristicOutcome::kAbortSyncSignin; |
| } |
| |
| auto enforce_enterprise_separation = EnterpriseSeparationMaybeRequired( |
| profile_, identity_manager_, email, is_new_account, |
| /*intercepted_profile_separation_policies=*/std::nullopt, |
| /*expects_intercepted_profile_separation_policies_for_testing=*/ |
| intercepted_account_profile_separation_policies_response_for_testing_ |
| .has_value()); |
| |
| // If we do not have all the information to enforce or not enterprise profile |
| // separation, return `std::nullopt` so that we can try and get more info on |
| // the intercepted account. |
| if (!enforce_enterprise_separation.has_value()) { |
| return std::nullopt; |
| } |
| |
| const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble( |
| email, |
| &g_browser_process->profile_manager()->GetProfileAttributesStorage()); |
| if (switch_to_entry && entry) { |
| *entry = switch_to_entry; |
| } |
| |
| if (enforce_enterprise_separation.value()) { |
| return switch_to_entry |
| ? SigninInterceptionHeuristicOutcome:: |
| kInterceptEnterpriseForcedProfileSwitch |
| : SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced; |
| } |
| |
| CHECK(!enforce_enterprise_separation.value()); |
| |
| // If interception is disabled and there are no enforce enterprise |
| // profile separation policies abort. |
| if (!signin_interception_enabled) { |
| return SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled; |
| } |
| |
| // Don't show profile switch for reauth. |
| if (switch_to_entry && is_new_account) { |
| return SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch; |
| } |
| |
| // The `gaia_id` must have a value to properly read the prefs related to the |
| // account and the Chrome Signin Bubble. |
| if (!gaia_id.empty()) { |
| // Chrome sign in bubble is shown if chrome isn't signed in. |
| ShouldShowChromeSigninBubbleWithReason should_show_chrome_signin_bubble = |
| MaybeShouldShowChromeSigninBubble(*profile_->GetPrefs(), |
| identity_manager_, gaia_id, |
| state_->access_point_); |
| if (update_state) { |
| state_->should_show_chrome_signin_bubble_ = |
| should_show_chrome_signin_bubble; |
| } |
| |
| // Showing the Chrome Signin Bubble is part of the Uno Desktop project. |
| if (switches::IsExplicitBrowserSigninUIOnDesktopEnabled() && |
| should_show_chrome_signin_bubble == |
| ShouldShowChromeSigninBubbleWithReason::kShouldShow) { |
| return SigninInterceptionHeuristicOutcome::kInterceptChromeSignin; |
| } |
| } |
| |
| // Do not intercept reauth. |
| // Reauth is subject to chrome sign in bubble which is checked earlier in this |
| // function. |
| if (!is_new_account) { |
| return SigninInterceptionHeuristicOutcome::kAbortAccountNotNew; |
| } |
| |
| // From this point the remaining possible interceptions involve creating a new |
| // profile. |
| if (!profiles::IsProfileCreationAllowed()) { |
| return SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed; |
| } |
| |
| if (IsFirstAccount(identity_manager_, email)) { |
| // Enterprise and multi-user bubbles are only shown if there are multiple |
| // accounts. The intercepted account may not be added to chrome yet. |
| return SigninInterceptionHeuristicOutcome::kAbortSingleAccount; |
| } |
| |
| if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) { |
| // This is not the first account in the identity manager but there is no |
| // primary account, all the accounts are in the UNO web-only state, so do |
| // not intercept. |
| DCHECK(switches::IsExplicitBrowserSigninUIOnDesktopEnabled()); |
| return SigninInterceptionHeuristicOutcome:: |
| kAbortNotFirstAccountButNoPrimaryAccount; |
| } |
| |
| if (HasUserDeclinedProfileCreation(email)) { |
| return SigninInterceptionHeuristicOutcome:: |
| kAbortUserDeclinedProfileForAccount; |
| } |
| |
| return std::nullopt; |
| } |
| |
| void DiceWebSigninInterceptor::MaybeInterceptWebSignin( |
| content::WebContents* web_contents, |
| CoreAccountId account_id, |
| signin_metrics::AccessPoint access_point, |
| bool is_new_account, |
| bool is_sync_signin) { |
| // If the user is in sign in pending state and signs in with a different |
| // account, it means that they enter an inconsistent state. Record this event |
| // so that we can check afterwards if this state is resolved by accepting the |
| // ProfileSwitchBubble or MultiUserBubble. |
| if (IsInInconsistentStateWithPrimaryAccount(account_id, identity_manager_)) { |
| base::UmaHistogramBoolean("Signin.SigninPending.InconsistentStateInvoked", |
| true); |
| } |
| |
| if (state_->is_interception_in_progress_) { |
| // Multiple concurrent interceptions are not supported. |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress); |
| return; |
| } |
| DCHECK_EQ(state_->interception_start_time_, base::TimeTicks()); |
| state_->interception_start_time_ = base::TimeTicks::Now(); |
| state_->access_point_ = access_point; |
| |
| if (!web_contents) { |
| // The tab has been closed (typically during the token exchange, which may |
| // take some time). |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortTabClosed); |
| Reset(); |
| return; |
| } |
| |
| if (!delegate_->IsSigninInterceptionSupported(*web_contents)) { |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortNoSupportedBrowser); |
| Reset(); |
| return; |
| } |
| |
| // Do not show the interception UI if a password update is required: both |
| // bubbles cannot be shown at the same time and the password update is more |
| // important. |
| ChromePasswordManagerClient* password_manager_client = |
| ChromePasswordManagerClient::FromWebContents(web_contents); |
| if (password_manager_client && password_manager_client->GetPasswordManager() |
| ->IsFormManagerPendingPasswordUpdate()) { |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortPasswordUpdatePending); |
| Reset(); |
| return; |
| } |
| |
| ManagePasswordsUIController* password_controller = |
| ManagePasswordsUIController::FromWebContents(web_contents); |
| if (password_controller && |
| password_controller->GetState() == |
| password_manager::ui::State::PENDING_PASSWORD_UPDATE_STATE) { |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortPasswordUpdate); |
| Reset(); |
| return; |
| } |
| |
| AccountInfo account_info = |
| identity_manager_->FindExtendedAccountInfoByAccountId(account_id); |
| DCHECK(!account_info.IsEmpty()) << "Intercepting unknown account."; |
| const ProfileAttributesEntry* entry = nullptr; |
| std::optional<SigninInterceptionHeuristicOutcome> heuristic_outcome = |
| GetHeuristicOutcome(is_new_account, is_sync_signin, account_info.email, |
| account_info.gaia, |
| /*update_state=*/true, &entry); |
| state_->account_id_ = account_id; |
| state_->is_interception_in_progress_ = true; |
| state_->new_account_interception_ = is_new_account; |
| state_->web_contents_ = web_contents->GetWeakPtr(); |
| |
| if (heuristic_outcome && |
| !SigninInterceptionHeuristicOutcomeIsSuccess(*heuristic_outcome)) { |
| RecordSigninInterceptionHeuristicOutcome(*heuristic_outcome); |
| if (state_->should_show_chrome_signin_bubble_) { |
| RecordShouldShowChromeSigninBubbleReason( |
| state_->should_show_chrome_signin_bubble_.value()); |
| } |
| Reset(); |
| return; |
| } |
| |
| // Start a timeout for the async operations we're kicking off. |
| state_->interception_info_available_timeout_.Reset( |
| base::BindOnce(&DiceWebSigninInterceptor::OnInterceptionInfoFetchTimeout, |
| base::Unretained(this))); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, state_->interception_info_available_timeout_.callback(), |
| base::Seconds(5)); |
| |
| // Process the interception (maybe kicking off async fetches). |
| ProcessInterceptionOrWait(account_info, /*timed_out=*/false); |
| } |
| |
| void DiceWebSigninInterceptor::CreateBrowserAfterSigninInterception( |
| CoreAccountId account_id, |
| content::WebContents* intercepted_contents, |
| std::unique_ptr<ScopedWebSigninInterceptionBubbleHandle> bubble_handle, |
| bool is_new_profile, |
| WebSigninInterceptor::SigninInterceptionType interception_type) { |
| DCHECK(!state_->session_startup_helper_); |
| DCHECK(bubble_handle); |
| state_->interception_bubble_handle_ = std::move(bubble_handle); |
| state_->account_id_ = account_id; |
| state_->interception_type_ = interception_type; |
| state_->session_startup_helper_ = |
| std::make_unique<DiceInterceptedSessionStartupHelper>( |
| profile_, is_new_profile, account_id, intercepted_contents); |
| state_->session_startup_helper_->Startup( |
| base::BindOnce(&DiceWebSigninInterceptor::OnNewBrowserCreated, |
| base::Unretained(this), is_new_profile)); |
| } |
| |
| void DiceWebSigninInterceptor::Shutdown() { |
| if (state_->is_interception_in_progress_ && |
| !state_->was_interception_ui_displayed_) { |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortShutdown); |
| } |
| Reset(); |
| } |
| |
| void DiceWebSigninInterceptor::Reset() { |
| state_ = std::make_unique<ResetableState>(); |
| account_info_update_observation_.Reset(); |
| } |
| |
| const ProfileAttributesEntry* |
| DiceWebSigninInterceptor::ShouldShowProfileSwitchBubble( |
| const std::string& intercepted_email, |
| ProfileAttributesStorage* profile_attribute_storage) const { |
| // Check if there is already an existing profile with this account. |
| base::FilePath profile_path = profile_->GetPath(); |
| for (const auto* entry : |
| profile_attribute_storage->GetAllProfilesAttributes()) { |
| if (entry->GetPath() == profile_path) { |
| continue; |
| } |
| if (gaia::AreEmailsSame(intercepted_email, |
| base::UTF16ToUTF8(entry->GetUserName()))) { |
| return entry; |
| } |
| } |
| return nullptr; |
| } |
| |
| bool DiceWebSigninInterceptor::ShouldEnforceEnterpriseProfileSeparation( |
| const AccountInfo& intercepted_account_info) const { |
| DCHECK(IsRequiredExtendedAccountInfoAvailable(intercepted_account_info)); |
| CoreAccountInfo primary_account = |
| identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); |
| // In case of re-auth of a managed primary account, do not show the enterprise |
| // separation dialog if the user already consented to enterprise management. |
| if (intercepted_account_info.IsManaged() && |
| IsReauthPrimaryAccount(state_->new_account_interception_, |
| intercepted_account_info.account_id, |
| identity_manager_)) { |
| // Sync users are considered to have implicitly accepted management. |
| // Returns true only for reauth for primary accounts without sync where |
| // management hasn't been accepted. |
| return !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync) && |
| !enterprise_util::UserAcceptedAccountManagement(profile_); |
| } |
| |
| if (!signin_util::IsAccountExemptedFromEnterpriseProfileSeparation( |
| profile_, intercepted_account_info.email)) { |
| return true; |
| } |
| |
| if (!signin_util::IsProfileSeparationEnforcedByProfile( |
| profile_, intercepted_account_info.email) && |
| !signin_util::IsProfileSeparationEnforcedByPolicies( |
| state_->intercepted_account_profile_separation_policies_.value_or( |
| policy::ProfileSeparationPolicies()))) { |
| return false; |
| } |
| |
| return state_->new_account_interception_ && |
| intercepted_account_info.IsManaged(); |
| } |
| |
| bool DiceWebSigninInterceptor::ShouldShowEnterpriseDialog( |
| const AccountInfo& intercepted_account_info) const { |
| DCHECK(IsRequiredExtendedAccountInfoAvailable(intercepted_account_info)); |
| |
| if (!base::FeatureList::IsEnabled( |
| features::kEnterpriseUpdatedProfileCreationScreen)) { |
| return false; |
| } |
| |
| if (state_->intercepted_account_profile_separation_policies_ |
| .value_or(policy::ProfileSeparationPolicies()) |
| .profile_separation_settings() |
| .value_or(policy::ProfileSeparationSettings::SUGGESTED) != |
| policy::ProfileSeparationSettings::SUGGESTED) { |
| return false; |
| } |
| |
| // Check if the intercepted account is managed and has not yet accepted |
| // management. |
| if (!intercepted_account_info.IsManaged() || |
| enterprise_util::UserAcceptedAccountManagement(profile_)) { |
| return false; |
| } |
| |
| if (IsPrimaryAccountInterception(intercepted_account_info.account_id, |
| identity_manager_)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool DiceWebSigninInterceptor::ShouldShowEnterpriseBubble( |
| const AccountInfo& intercepted_account_info) const { |
| DCHECK(IsRequiredExtendedAccountInfoAvailable(intercepted_account_info)); |
| // Check if the intercepted account or the primary account is managed. |
| AccountInfo primary_acccount = GetPrimaryAccountInfo(identity_manager_); |
| |
| if (primary_acccount.IsEmpty() || |
| IsPrimaryAccountInterception(intercepted_account_info.account_id, |
| identity_manager_)) { |
| return false; |
| } |
| |
| return intercepted_account_info.IsManaged() || primary_acccount.IsManaged(); |
| } |
| |
| bool DiceWebSigninInterceptor::ShouldShowMultiUserBubble( |
| const AccountInfo& intercepted_account_info) const { |
| DCHECK(IsRequiredExtendedAccountInfoAvailable(intercepted_account_info)); |
| if (identity_manager_->GetAccountsWithRefreshTokens().size() <= 1u || |
| !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) { |
| return false; |
| } |
| // Check if the account has the same name as another account in the profile. |
| for (const auto& account_info : |
| identity_manager_->GetExtendedAccountInfoForAccountsWithRefreshToken()) { |
| if (account_info.account_id == intercepted_account_info.account_id) { |
| continue; |
| } |
| // Case-insensitve comparison supporting non-ASCII characters. |
| if (base::i18n::FoldCase(base::UTF8ToUTF16(account_info.given_name)) == |
| base::i18n::FoldCase( |
| base::UTF8ToUTF16(intercepted_account_info.given_name))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool DiceWebSigninInterceptor::ShouldShowChromeSigninBubble( |
| const GaiaId& gaia_id) { |
| state_->should_show_chrome_signin_bubble_ = MaybeShouldShowChromeSigninBubble( |
| *profile_->GetPrefs(), identity_manager_, gaia_id, state_->access_point_); |
| CHECK(state_->should_show_chrome_signin_bubble_.has_value()); |
| RecordShouldShowChromeSigninBubbleReason( |
| state_->should_show_chrome_signin_bubble_.value()); |
| |
| return switches::IsExplicitBrowserSigninUIOnDesktopEnabled() && |
| state_->should_show_chrome_signin_bubble_ == |
| ShouldShowChromeSigninBubbleWithReason::kShouldShow; |
| } |
| |
| void DiceWebSigninInterceptor::ShowSigninInterceptionBubble( |
| const WebSigninInterceptor::Delegate::BubbleParameters& bubble_parameters, |
| base::OnceCallback<void(SigninInterceptionResult)> callback) { |
| state_->was_interception_ui_displayed_ = true; |
| state_->interception_type_ = bubble_parameters.interception_type; |
| state_->interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble( |
| state_->web_contents_.get(), bubble_parameters, std::move(callback)); |
| } |
| |
| void DiceWebSigninInterceptor::EnsureObservingExtendedAccountInfo() { |
| // Start an observation if one isn't already in progress. |
| if (!account_info_update_observation_.IsObserving()) { |
| account_info_update_observation_.Observe(identity_manager_.get()); |
| } |
| } |
| |
| void DiceWebSigninInterceptor::ProcessInterceptionOrWait( |
| const AccountInfo& info, |
| bool timed_out) { |
| DCHECK_EQ(info.account_id, state_->account_id_); |
| |
| if (!IsRequiredExtendedAccountInfoAvailable(info)) { |
| // We can't process the interception with the information currently |
| // available. |
| // |
| // If this is a timeout we abort the interception, otherwise we wait for |
| // the remaining information to be fetched asynchronously. |
| if (timed_out) { |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortAccountInfoTimeout); |
| Reset(); |
| return; |
| } |
| EnsureObservingExtendedAccountInfo(); |
| return; |
| } |
| |
| if (timed_out) { |
| // We've timed out waiting for some optional information - process the |
| // interception with what we have. |
| OnInterceptionReadyToBeProcessed(info); |
| return; |
| } |
| |
| bool have_all_extended_account_info = |
| IsFullExtendedAccountInfoAvailable(info); |
| bool have_all_enterprise_info = |
| EnterpriseSeparationMaybeRequired( |
| profile_, identity_manager_, info.email, |
| state_->new_account_interception_, |
| state_->intercepted_account_profile_separation_policies_, |
| /*expects_intercepted_profile_separation_policies_for_testing=*/ |
| intercepted_account_profile_separation_policies_response_for_testing_ |
| .has_value()) |
| .has_value(); |
| |
| if (!have_all_extended_account_info) { |
| // We're need more extended account info - ensure we're waiting on that. |
| EnsureObservingExtendedAccountInfo(); |
| } else { |
| account_info_update_observation_.Reset(); |
| } |
| |
| if (!have_all_enterprise_info) { |
| // Fetch the ManagedAccountsSigninRestriction policy value for the |
| // intercepted account with a timeout. |
| EnsureAccountLevelSigninRestrictionFetchInProgress( |
| info, base::BindOnce( |
| &DiceWebSigninInterceptor:: |
| OnAccountLevelManagedAccountsSigninRestrictionReceived, |
| base::Unretained(this), info)); |
| } |
| |
| if (have_all_extended_account_info && have_all_enterprise_info) { |
| // We have all the information we need - process the interception. |
| state_->interception_info_available_timeout_.Cancel(); |
| OnInterceptionReadyToBeProcessed(info); |
| return; |
| } |
| } |
| |
| void DiceWebSigninInterceptor::OnInterceptionReadyToBeProcessed( |
| const AccountInfo& info) { |
| DCHECK_EQ(info.account_id, state_->account_id_); |
| DCHECK(IsRequiredExtendedAccountInfoAvailable(info)); |
| |
| std::optional<WebSigninInterceptor::SigninInterceptionType> interception_type; |
| |
| ProfileAttributesEntry* entry = |
| g_browser_process->profile_manager() |
| ->GetProfileAttributesStorage() |
| .GetProfileAttributesWithPath(profile_->GetPath()); |
| SkColor profile_color = GenerateNewProfileColor(entry).color; |
| |
| const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble( |
| info.email, |
| &g_browser_process->profile_manager()->GetProfileAttributesStorage()); |
| |
| bool force_profile_separation = |
| ShouldEnforceEnterpriseProfileSeparation(info); |
| bool reauth = !state_->new_account_interception_; |
| bool show_link_data_option = false; |
| |
| if (force_profile_separation) { |
| if (switch_to_entry) { |
| interception_type = |
| WebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced; |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome:: |
| kInterceptEnterpriseForcedProfileSwitch); |
| } else { |
| interception_type = |
| WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced; |
| auto primary_account_id = |
| identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSignin); |
| show_link_data_option = |
| (primary_account_id.empty() || |
| primary_account_id == info.account_id) && |
| signin_util:: |
| ProfileSeparationAllowsKeepingUnmanagedBrowsingDataInManagedProfile( |
| profile_, |
| state_->intercepted_account_profile_separation_policies_ |
| .value_or(policy::ProfileSeparationPolicies())); |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); |
| } |
| } else if (ShouldShowEnterpriseDialog(info)) { |
| interception_type = WebSigninInterceptor::SigninInterceptionType:: |
| kEnterpriseAcceptManagement; |
| show_link_data_option = true; |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kInterceptEnterprise); |
| } else if (!profile_->GetPrefs()->GetBoolean( |
| prefs::kSigninInterceptionEnabled)) { |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled); |
| Reset(); |
| return; |
| } else if (!reauth && switch_to_entry) { |
| // Propose account switching if we skipped in GetHeuristicOutcome because we |
| // returned a nullptr to get more information about forced enterprise |
| // profile separation. |
| interception_type = |
| WebSigninInterceptor::SigninInterceptionType::kProfileSwitch; |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); |
| } else if (ShouldShowChromeSigninBubble(info.gaia)) { |
| interception_type = |
| WebSigninInterceptor::SigninInterceptionType::kChromeSignin; |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kInterceptChromeSignin); |
| } else if (reauth) { |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortAccountNotNew); |
| Reset(); |
| return; |
| } else if (HasUserDeclinedProfileCreation(info.email)) { |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome:: |
| kAbortUserDeclinedProfileForAccount); |
| Reset(); |
| return; |
| } else if (ShouldShowEnterpriseBubble(info)) { |
| interception_type = |
| WebSigninInterceptor::SigninInterceptionType::kEnterprise; |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kInterceptEnterprise); |
| } else if (ShouldShowMultiUserBubble(info)) { |
| interception_type = |
| WebSigninInterceptor::SigninInterceptionType::kMultiUser; |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kInterceptMultiUser); |
| } |
| |
| if (!interception_type) { |
| // Signin should not be intercepted. |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible); |
| Reset(); |
| return; |
| } |
| |
| bool show_managed_disclaimer = |
| *interception_type != |
| WebSigninInterceptor::SigninInterceptionType::kProfileSwitch && |
| (info.IsManaged() || |
| policy::ManagementServiceFactory::GetForPlatform()->IsManaged()); |
| |
| MaybeRecordSupervisedUserStateMetrics(info, interception_type.value()); |
| |
| WebSigninInterceptor::Delegate::BubbleParameters bubble_parameters( |
| *interception_type, info, GetPrimaryAccountInfo(identity_manager_), |
| GetAutogeneratedThemeColors(profile_color).frame_color, |
| show_link_data_option, show_managed_disclaimer); |
| |
| base::OnceCallback<void(SigninInterceptionResult)> callback; |
| switch (*interception_type) { |
| case WebSigninInterceptor::SigninInterceptionType::kProfileSwitch: |
| case WebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced: |
| callback = base::BindOnce( |
| &DiceWebSigninInterceptor::OnProfileSwitchChoice, |
| base::Unretained(this), info.email, switch_to_entry->GetPath()); |
| break; |
| case WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced: |
| case WebSigninInterceptor::SigninInterceptionType:: |
| kEnterpriseAcceptManagement: |
| callback = base::BindOnce( |
| &DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult, |
| base::Unretained(this), info, profile_color); |
| break; |
| case WebSigninInterceptor::SigninInterceptionType::kEnterprise: |
| case WebSigninInterceptor::SigninInterceptionType::kMultiUser: |
| callback = |
| base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice, |
| base::Unretained(this), info, profile_color); |
| break; |
| case WebSigninInterceptor::SigninInterceptionType::kChromeSignin: |
| callback = base::BindOnce(&DiceWebSigninInterceptor::OnChromeSigninChoice, |
| base::Unretained(this), info); |
| break; |
| case WebSigninInterceptor::SigninInterceptionType::kEnterpriseOIDC: |
| NOTREACHED() << "This interception type should not happen in DICE"; |
| } |
| ShowSigninInterceptionBubble(bubble_parameters, std::move(callback)); |
| } |
| |
| void DiceWebSigninInterceptor::OnExtendedAccountInfoUpdated( |
| const AccountInfo& info) { |
| if (info.account_id != state_->account_id_) { |
| return; |
| } |
| ProcessInterceptionOrWait(info, false); |
| } |
| |
| void DiceWebSigninInterceptor::OnExtendedAccountInfoRemoved( |
| const AccountInfo& info) { |
| if (info.account_id != state_->account_id_) { |
| return; |
| } |
| RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome::kAbortSignedOut); |
| Reset(); |
| } |
| |
| void DiceWebSigninInterceptor::OnInterceptionInfoFetchTimeout() { |
| account_info_update_observation_.Reset(); |
| if (!state_->intercepted_account_profile_separation_policies_.has_value()) { |
| state_->intercepted_account_profile_separation_policies_ = |
| policy::ProfileSeparationPolicies(); |
| } |
| |
| AccountInfo account_info = |
| identity_manager_->FindExtendedAccountInfoByAccountId( |
| state_->account_id_); |
| ProcessInterceptionOrWait(account_info, /*timed_out=*/true); |
| } |
| |
| void DiceWebSigninInterceptor::OnProfileCreationChoice( |
| const AccountInfo& account_info, |
| SkColor profile_color, |
| SigninInterceptionResult create) { |
| if (create != SigninInterceptionResult::kAccepted) { |
| if (create == SigninInterceptionResult::kDeclined) { |
| IncrementEmailToCountDictionaryPref( |
| prefs::kProfileCreationInterceptionDeclined, account_info.email); |
| } |
| Reset(); |
| return; |
| } |
| |
| DCHECK(state_->interception_bubble_handle_); |
| std::u16string profile_name = |
| profiles::GetDefaultNameForNewSignedInProfile(account_info); |
| ProfilePresets profile_presets(profile_color); |
| profile_presets.search_engine_choice_data = |
| SearchEngineChoiceDialogService::GetChoiceDataFromProfile(*profile_); |
| |
| DCHECK(!state_->dice_signed_in_profile_creator_); |
| // Unretained is fine because the profile creator is owned by this. |
| state_->dice_signed_in_profile_creator_ = |
| std::make_unique<DiceSignedInProfileCreator>( |
| profile_, state_->account_id_, profile_name, |
| profiles::GetPlaceholderAvatarIndex(), |
| base::BindOnce( |
| &DiceWebSigninInterceptor::OnNewSignedInProfileCreated, |
| base::Unretained(this), |
| std::optional<ProfilePresets>(std::move(profile_presets)))); |
| } |
| |
| SigninInterceptionResult |
| DiceWebSigninInterceptor::ProcessChromeSigninUserChoice( |
| SigninInterceptionResult result, |
| const GaiaId& gaia_id) { |
| CHECK(switches::IsExplicitBrowserSigninUIOnDesktopEnabled()); |
| SigninPrefs signin_prefs(*profile_->GetPrefs()); |
| // When in `ChromeSigninUserChoice::kAlwaysAsk` setting mode, the bubble |
| // result should not be remembered or affect the setting mode. |
| if (signin_prefs.GetChromeSigninInterceptionUserChoice(gaia_id) == |
| ChromeSigninUserChoice::kAlwaysAsk) { |
| return result; |
| } |
| |
| SigninInterceptionResult processed_result = result; |
| // Treat dismiss case: might turn into a decline if max dismiss count is |
| // reached. |
| if (processed_result == SigninInterceptionResult::kDismissed) { |
| size_t dismiss_count = |
| signin_prefs.IncrementChromeSigninInterceptionDismissCount(gaia_id); |
| if (dismiss_count >= kMaxChromeSigninInterceptionDismissCount) { |
| // Proceed with the result treated as declined since we reached the max |
| // dismissal count, or the user is in the always ask mode. |
| // TODO(crbug.com/319396084): Should we record something here? |
| processed_result = SigninInterceptionResult::kDeclined; |
| } else { |
| // Max dismiss count not reached yet, proceed with a simple dismiss. |
| return result; |
| } |
| } |
| |
| if (processed_result == SigninInterceptionResult::kAccepted) { |
| // Save user choice as always sign in. |
| signin_prefs.SetChromeSigninInterceptionUserChoice( |
| gaia_id, ChromeSigninUserChoice::kSignin); |
| } |
| |
| if (processed_result == SigninInterceptionResult::kDeclined) { |
| // If the user had no explicit interaction with the bubble or the Chrome |
| // Signin setting previously, then set it's first decline time. |
| if (signin_prefs.GetChromeSigninInterceptionUserChoice(gaia_id) == |
| ChromeSigninUserChoice::kNoChoice) { |
| signin_prefs.SetChromeSigninInterceptionLastBubbleDeclineTime( |
| gaia_id, base::Time::Now()); |
| } |
| |
| MaybeUpdateRepromptInfoAfterDecline(signin_prefs, gaia_id); |
| |
| // Save user choice as do not sign in automatically. |
| signin_prefs.SetChromeSigninInterceptionUserChoice( |
| gaia_id, ChromeSigninUserChoice::kDoNotSignin); |
| } |
| |
| return processed_result; |
| } |
| |
| void DiceWebSigninInterceptor::OnChromeSigninChoice( |
| const AccountInfo& account_info, |
| SigninInterceptionResult result) { |
| SigninInterceptionResult processed_result = |
| ProcessChromeSigninUserChoice(result, account_info.gaia); |
| |
| switch (processed_result) { |
| case SigninInterceptionResult::kIgnored: |
| // Can happen if the browser is closed while the bubble is still opened. |
| case SigninInterceptionResult::kNotDisplayed: |
| // Can happen if the web contents is destroyed between the time the bubble |
| // was requested to be displayed and actually being displayed. |
| case SigninInterceptionResult::kDismissed: |
| // Happens if the user closed the bubble without explicitly accepting or |
| // declining. |
| break; |
| case SigninInterceptionResult::kDeclined: |
| RecordChromeSigninNumberOfDismissesForAccount(account_info.gaia, |
| processed_result); |
| break; |
| case SigninInterceptionResult::kAcceptedWithExistingProfile: |
| NOTREACHED() |
| << "Those results are not expected within the Chrome Signin Bubble."; |
| case SigninInterceptionResult::kAccepted: |
| RecordChromeSigninNumberOfDismissesForAccount(account_info.gaia, |
| processed_result); |
| |
| auto access_point = signin_metrics::AccessPoint:: |
| ACCESS_POINT_CHROME_SIGNIN_INTERCEPT_BUBBLE; |
| signin_metrics::LogSignInStarted(access_point); |
| identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount( |
| account_info.account_id, signin::ConsentLevel::kSignin, access_point); |
| } |
| |
| // In all cases we want to close the bubble after the choice is taken. |
| Reset(); |
| } |
| |
| void DiceWebSigninInterceptor::OnProfileSwitchChoice( |
| const std::string& email, |
| const base::FilePath& profile_path, |
| SigninInterceptionResult switch_profile) { |
| if (switch_profile != SigninInterceptionResult::kAccepted) { |
| Reset(); |
| return; |
| } |
| |
| DCHECK(state_->interception_bubble_handle_); |
| DCHECK(!state_->dice_signed_in_profile_creator_); |
| // Unretained is fine because the profile creator is owned by this. |
| state_->dice_signed_in_profile_creator_ = |
| std::make_unique<DiceSignedInProfileCreator>( |
| profile_, state_->account_id_, profile_path, |
| base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated, |
| base::Unretained(this), std::nullopt)); |
| } |
| |
| void DiceWebSigninInterceptor::OnNewSignedInProfileCreated( |
| std::optional<ProfilePresets> profile_presets, |
| Profile* new_profile) { |
| DCHECK(state_->dice_signed_in_profile_creator_); |
| state_->dice_signed_in_profile_creator_.reset(); |
| |
| if (!new_profile) { |
| Reset(); |
| return; |
| } |
| |
| // The profile presets are defined only when the profile has just been created |
| // (with interception type kMultiUser or kEnterprise). If the profile is not |
| // new (kProfileSwitch) or if it is a guest profile, then the presets are not |
| // updated. |
| bool is_new_profile = profile_presets.has_value(); |
| if (is_new_profile) { |
| ProfileMetrics::LogProfileAddNewUser( |
| ProfileMetrics::ADD_NEW_USER_SIGNIN_INTERCEPTION); |
| // TODO(crbug.com/40775669): Remove the condition if Guest mode |
| // option is removed. |
| if (!new_profile->IsGuestSession()) { |
| // Apply the new color to the profile. |
| ThemeServiceFactory::GetForProfile(new_profile) |
| ->SetUserColorAndBrowserColorVariant( |
| profile_presets->profile_color, |
| ui::mojom::BrowserColorVariant::kTonalSpot); |
| |
| // The new profile inherits the default search provider and the search |
| // engine choice timestamp from the previous profile. |
| SearchEngineChoiceDialogService::UpdateProfileFromChoiceData( |
| *new_profile, profile_presets->search_engine_choice_data); |
| } |
| |
| // TODO(crbug.com/40269992): Move this to DiceSignedInProfileCreator when |
| // DisallowManagedProfileSignout is fully released. |
| if (state_->intercepted_account_management_accepted_ && |
| base::FeatureList::IsEnabled(kDisallowManagedProfileSignout)) { |
| auto* primary_account_mutator = |
| IdentityManagerFactory::GetForProfile(new_profile) |
| ->GetPrimaryAccountMutator(); |
| primary_account_mutator->SetPrimaryAccount( |
| state_->account_id_, signin::ConsentLevel::kSignin, |
| signin_metrics::AccessPoint::ACCESS_POINT_WEB_SIGNIN); |
| } |
| |
| // Set the ChromeSignin setting to always signin following accepting the |
| // signin intercept and being signed in. |
| if (switches::IsExplicitBrowserSigninUIOnDesktopEnabled()) { |
| CoreAccountInfo account_info = |
| IdentityManagerFactory::GetForProfile(new_profile) |
| ->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); |
| if (!account_info.IsEmpty()) { |
| SigninPrefs(*new_profile->GetPrefs()) |
| .SetChromeSigninInterceptionUserChoice( |
| account_info.gaia, ChromeSigninUserChoice::kSignin); |
| } |
| } |
| } |
| |
| enterprise_util::SetUserAcceptedAccountManagement( |
| new_profile, state_->intercepted_account_management_accepted_); |
| |
| // Work is done in this profile, the flow continues in the |
| // DiceWebSigninInterceptor that is attached to the new profile. |
| // We pass relevant parameters from this instance to the new one. |
| DiceWebSigninInterceptorFactory::GetForProfile(new_profile) |
| ->CreateBrowserAfterSigninInterception( |
| state_->account_id_, state_->web_contents_.get(), |
| std::move(state_->interception_bubble_handle_), is_new_profile, |
| *state_->interception_type_); |
| Reset(); |
| } |
| |
| void DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult( |
| const AccountInfo& account_info, |
| SkColor profile_color, |
| SigninInterceptionResult create) { |
| signin_util::RecordEnterpriseProfileCreationUserChoice( |
| /*enforced_by_policy=*/!signin_util::IsProfileSeparationEnforcedByProfile( |
| profile_, account_info.email) && |
| !signin_util::IsProfileSeparationEnforcedByPolicies( |
| state_->intercepted_account_profile_separation_policies_.value_or( |
| policy::ProfileSeparationPolicies())), |
| /*created=*/create == SigninInterceptionResult::kAccepted); |
| |
| // Make sure existing account is a non-signed in profile. |
| if (create == SigninInterceptionResult::kAccepted) { |
| state_->intercepted_account_management_accepted_ = true; |
| // In case of a reauth if there was no consent for management, do not create |
| // a new profile. |
| if (IsReauthPrimaryAccount(state_->new_account_interception_, |
| account_info.account_id, identity_manager_)) { |
| enterprise_util::SetUserAcceptedAccountManagement( |
| profile_, state_->intercepted_account_management_accepted_); |
| Reset(); |
| } else { |
| OnProfileCreationChoice(account_info, profile_color, |
| SigninInterceptionResult::kAccepted); |
| } |
| } else if (create == SigninInterceptionResult::kAcceptedWithExistingProfile) { |
| state_->intercepted_account_management_accepted_ = true; |
| if (GetPrimaryAccountInfo(identity_manager_).IsEmpty()) { |
| identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount( |
| account_info.account_id, signin::ConsentLevel::kSignin, |
| signin_metrics::AccessPoint:: |
| ACCESS_POINT_CHROME_SIGNIN_INTERCEPT_BUBBLE); |
| } else { |
| DCHECK_EQ(GetPrimaryAccountInfo(identity_manager_).account_id, |
| account_info.account_id); |
| } |
| |
| enterprise_util::SetUserAcceptedAccountManagement( |
| profile_, state_->intercepted_account_management_accepted_); |
| Reset(); |
| } else { |
| DCHECK_EQ(SigninInterceptionResult::kDeclined, create) |
| << "The user can only accept or decline"; |
| if (state_->interception_type_ == |
| WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced) { |
| auto* accounts_mutator = identity_manager_->GetAccountsMutator(); |
| accounts_mutator->RemoveAccount( |
| account_info.account_id, |
| signin_metrics::SourceForRefreshTokenOperation:: |
| kEnterpriseForcedProfileCreation_UserDecline); |
| } |
| OnProfileCreationChoice(account_info, profile_color, |
| SigninInterceptionResult::kDeclined); |
| } |
| } |
| |
| void DiceWebSigninInterceptor::OnNewBrowserCreated(bool is_new_profile) { |
| DCHECK(state_->interception_bubble_handle_); |
| state_->interception_bubble_handle_.reset(); // Close the bubble now. |
| state_->session_startup_helper_.reset(); |
| |
| // TODO(crbug.com/40775669): Remove |IsGuestSession| if Guest option is |
| // no more supported. |
| if (!is_new_profile || profile_->IsGuestSession()) { |
| return; |
| } |
| |
| Browser* browser = chrome::FindBrowserWithProfile(profile_); |
| DCHECK(browser); |
| delegate_->ShowFirstRunExperienceInNewProfile(browser, state_->account_id_, |
| *state_->interception_type_); |
| } |
| |
| // static |
| std::string DiceWebSigninInterceptor::GetPersistentEmailHash( |
| const std::string& email) { |
| int hash = base::PersistentHash( |
| gaia::CanonicalizeEmail(gaia::SanitizeEmail(email))) & |
| 0xFF; |
| return base::StringPrintf("email_%i", hash); |
| } |
| |
| size_t DiceWebSigninInterceptor::IncrementEmailToCountDictionaryPref( |
| const char* pref_name, |
| const std::string& email) { |
| // TODO(b/314079566): Consider merging the different similar pref counts into |
| // a single pref where the email hash maps to multiple values. |
| ScopedDictPrefUpdate update(profile_->GetPrefs(), pref_name); |
| std::string key = GetPersistentEmailHash(email); |
| std::optional<int> count = update->FindInt(key); |
| |
| int value = count.value_or(0) + 1; |
| update->Set(key, value); |
| |
| CHECK_GE(value, 0); |
| return value; |
| } |
| |
| void DiceWebSigninInterceptor::RecordChromeSigninNumberOfDismissesForAccount( |
| const GaiaId& gaia_id, |
| SigninInterceptionResult result) { |
| CHECK(switches::IsExplicitBrowserSigninUIOnDesktopEnabled()); |
| CHECK(result == SigninInterceptionResult::kAccepted || |
| result == SigninInterceptionResult::kDeclined) |
| << "Recording results only for accepting/declining the bubble. " |
| "Result: " |
| << static_cast<int>(result); |
| |
| std::string_view action_string = |
| result == SigninInterceptionResult::kAccepted ? "Accept" : "Decline"; |
| base::UmaHistogramCounts100( |
| base::StrCat( |
| {"Signin.Intercept.ChromeSignin.DismissesBefore", action_string}), |
| SigninPrefs(*profile_->GetPrefs()) |
| .GetChromeSigninInterceptionDismissCount(gaia_id)); |
| } |
| |
| bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation( |
| const std::string& email) const { |
| const base::Value::Dict& pref_data = profile_->GetPrefs()->GetDict( |
| prefs::kProfileCreationInterceptionDeclined); |
| std::optional<int> declined_count = |
| pref_data.FindInt(GetPersistentEmailHash(email)); |
| // Check if the user declined 2 times. |
| constexpr int kMaxProfileCreationDeclinedCount = 2; |
| return declined_count && |
| declined_count.value() >= kMaxProfileCreationDeclinedCount; |
| } |
| |
| void DiceWebSigninInterceptor:: |
| EnsureAccountLevelSigninRestrictionFetchInProgress( |
| const AccountInfo& account_info, |
| base::OnceCallback<void(const policy::ProfileSeparationPolicies&)> |
| callback) { |
| if (state_->account_level_signin_restriction_policy_fetcher_ != nullptr) { |
| // A fetch is already in progress, don't start a new one. |
| DCHECK_EQ(account_info.account_id, state_->account_id_); |
| return; |
| } |
| |
| if (intercepted_account_profile_separation_policies_response_for_testing_ |
| .has_value()) { |
| std::move(callback).Run( |
| intercepted_account_profile_separation_policies_response_for_testing_ |
| .value()); |
| return; |
| } |
| |
| DCHECK(!state_->interception_info_available_timeout_.IsCancelled()); |
| |
| state_->account_level_signin_restriction_policy_fetcher_ = |
| std::make_unique<policy::UserCloudSigninRestrictionPolicyFetcher>( |
| g_browser_process->browser_policy_connector(), |
| g_browser_process->system_network_context_manager() |
| ->GetSharedURLLoaderFactory()); |
| state_->account_level_signin_restriction_policy_fetcher_ |
| ->GetManagedAccountsSigninRestriction( |
| identity_manager_, account_info.account_id, std::move(callback), |
| policy::utils::IsPolicyTestingEnabled(profile_->GetPrefs(), |
| chrome::GetChannel()) |
| ? profile_->GetPrefs() |
| ->GetDefaultPrefValue( |
| prefs::kUserCloudSigninPolicyResponseFromPolicyTestPage) |
| ->GetString() |
| : std::string()); |
| } |
| |
| void DiceWebSigninInterceptor:: |
| OnAccountLevelManagedAccountsSigninRestrictionReceived( |
| const AccountInfo& account_info, |
| const policy::ProfileSeparationPolicies& profile_separation_policies) { |
| state_->intercepted_account_profile_separation_policies_ = |
| profile_separation_policies; |
| ProcessInterceptionOrWait(account_info, /*timed_out=*/false); |
| } |
| |
| void DiceWebSigninInterceptor::RecordSigninInterceptionHeuristicOutcome( |
| SigninInterceptionHeuristicOutcome outcome) const { |
| // Record the outcome. |
| base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome); |
| |
| // Record the latency, except in the case where this is a duplicate request |
| // for the same interception. |
| DCHECK_NE(state_->interception_start_time_, base::TimeTicks()); |
| if (outcome == |
| SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress) { |
| // This is a special-case where we immediately abort the intercept request |
| // without first updating interception_start_time_ (because the previous |
| // request has not completed). |
| // Record the histogram for this request with zero duration. |
| base::UmaHistogramTimes("Signin.Intercept.HeuristicLatency", |
| base::Milliseconds(0)); |
| } else { |
| base::UmaHistogramTimes( |
| "Signin.Intercept.HeuristicLatency", |
| base::TimeTicks::Now() - state_->interception_start_time_); |
| } |
| } |
| |
| bool DiceWebSigninInterceptor::IsFullExtendedAccountInfoAvailable( |
| const AccountInfo& account_info) const { |
| if (!IsRequiredExtendedAccountInfoAvailable(account_info)) { |
| return false; |
| } |
| return account_info.capabilities.is_subject_to_parental_controls() != |
| signin::Tribool::kUnknown; |
| } |
| |
| bool DiceWebSigninInterceptor::managed_profile_creation_required_by_policy() |
| const { |
| return state_->interception_type_ == |
| WebSigninInterceptor::SigninInterceptionType::kEnterpriseForced; |
| } |
| |
| AccountInfo DiceWebSigninInterceptor::intercepted_account_info() const { |
| return identity_manager_->FindExtendedAccountInfoByAccountId( |
| state_->account_id_); |
| } |
| |
| // static |
| base::TimeDelta |
| DiceWebSigninInterceptor::GetTimeSinceLastChromeSigninDeclineForTesting( |
| const SigninPrefs& signin_prefs, |
| const GaiaId& gaia_id) { |
| return GetTimeSinceLastChromeSigninDecline(signin_prefs, gaia_id); |
| } |