blob: 36b8df5a77a686e9ccdc050b19d3ffb2a4317aa3 [file] [log] [blame]
// 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);
}