blob: 5729d4fdf92e9aedf334aeb270f099a4238569f0 [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.
#import "ios/chrome/browser/authentication/ui_bundled/signin/signin_utils.h"
#import "base/barrier_closure.h"
#import "base/command_line.h"
#import "base/functional/bind.h"
#import "base/rand_util.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "base/version.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/policy/policy_constants.h"
#import "components/prefs/pref_service.h"
#import "components/signin/ios/browser/features.h"
#import "components/signin/public/base/signin_switches.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/signin/public/identity_manager/tribool.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/change_profile_commands.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/browser/authentication/ui_bundled/change_profile/change_profile_settings_continuation.h"
#import "ios/chrome/browser/authentication/ui_bundled/change_profile/change_profile_signout_continuation.h"
#import "ios/chrome/browser/authentication/ui_bundled/history_sync/history_sync_coordinator.h"
#import "ios/chrome/browser/authentication/ui_bundled/history_sync/history_sync_utils.h"
#import "ios/chrome/browser/authentication/ui_bundled/signin/features.h"
#import "ios/chrome/browser/authentication/ui_bundled/signin/signin_constants.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/features.h"
#import "ios/chrome/browser/shared/model/profile/profile_attributes_storage_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios_util.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/signin_util.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "net/base/network_change_notifier.h"
#import "third_party/abseil-cpp/absl/container/flat_hash_map.h"
namespace {
// Maximum delay to wait for fetching the account capabilities before showing
// the sign-in upgrade promo. If fetching the account capabilities takes more
// than the delay, then the promo is suppressed - it may be shown on the next
// start-up.
constexpr base::TimeDelta kShowSigninUpgradePromoMaxDelay =
base::Milliseconds(200);
// The duration between two signin upgrade promo trigger is randomly chosen
// between [53..68) days.
base::TimeDelta DurationBetweenPromoTriggers() {
return base::RandTimeDelta(base::Days(53), base::Days(68));
}
// Initiate synchronously the change to `profile`, then run `continuation`
// when the change completes asynchronously. The UI (thus `scene_state`)
// will be destroyed synchronously, so this function should not be called
// directly, instead it should be posted as a task.
//
// Destroying the UI will destroy the SceneState, the SceneController and
// the Browser. As the SceneState is an Objective-C class and the Browser
// is a C++ class, this method take a SceneState* as parameter to avoid
// risking accessing a dangling pointer to a C++ object.
void SwitchToProfileSynchronously(const std::string& profile_name,
__weak SceneState* weak_scene_state,
ChangeProfileReason reason,
ChangeProfileContinuation continuation) {
if (SceneState* scene_state = weak_scene_state) {
id<ChangeProfileCommands> change_profile_handler = HandlerForProtocol(
scene_state.profileState.appState.appCommandDispatcher,
ChangeProfileCommands);
[change_profile_handler changeProfile:profile_name
forScene:scene_state
reason:reason
continuation:std::move(continuation)];
}
}
// Converts an array of AccountInfos to a set of gaia ids.
NSSet<NSString*>* GaiaIdSetWithAccountInfos(
const std::vector<AccountInfo>& account_infos) {
NSMutableSet* gaia_id_set = [NSMutableSet set];
for (const AccountInfo& account_info : account_infos) {
[gaia_id_set addObject:account_info.gaia.ToNSString()];
}
return [gaia_id_set copy];
}
// Returns whether the gaia ids `recorded_gaia_ids` is a strict subset of the
// current `identities_on_device_gaia_ids` (i.e. all the recorded gaia IDs are
// (still) on the device, but there is at least one new identity on the device).
bool IsStrictSubset(NSArray<NSString*>* recorded_gaia_ids,
NSSet<NSString*>* identities_on_device_gaia_ids) {
// Optimisation for the case of a nil or empty `recorded_gaia_ids`.
// This allow not special casing the construction of the NSSet (as
// -[NSSet setWithArray:] does not support nil for the array).
if (recorded_gaia_ids.count == 0) {
return identities_on_device_gaia_ids.count > 0;
}
NSSet<NSString*>* recorded_gaia_ids_set =
[NSSet setWithArray:recorded_gaia_ids];
return [recorded_gaia_ids_set isSubsetOfSet:identities_on_device_gaia_ids] &&
![recorded_gaia_ids_set isEqualToSet:identities_on_device_gaia_ids];
}
// Returns true if profile separation is enabled and the current profile is not
// the personal one (a managed profile).
bool ShouldSwitchProfileAtSignout(AuthenticationService* authentication_service,
ProfileIOS* profile) {
bool is_work_profile = !IsPersonalProfile(profile);
return AreSeparateProfilesForManagedAccountsEnabled() &&
authentication_service->HasPrimaryIdentityManaged(
signin::ConsentLevel::kSignin) &&
is_work_profile;
}
// Post an asynchronous request to switch to `profile`, running `continuation`
// when the change completes.
void SwitchToProfile(SceneState* scene_state,
const std::string& profile_name,
ChangeProfileReason reason,
ChangeProfileContinuation continuation) {
__weak SceneState* weak_scene_state = scene_state;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&SwitchToProfileSynchronously, profile_name,
weak_scene_state, reason, std::move(continuation)));
}
syncer::DataTypeSet DataCountsMapToDataTypeSet(
absl::flat_hash_map<syncer::DataType, size_t> type_counts) {
syncer::DataTypeSet types;
for (const auto& [type, count] : type_counts) {
types.Put(type);
}
return types;
}
} // namespace
#pragma mark - Public
namespace signin {
base::TimeDelta GetWaitThresholdForCapabilities() {
const base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(
signin::kWaitThresholdMillisecondsForCapabilitiesApi)) {
std::string delayString = command_line->GetSwitchValueASCII(
signin::kWaitThresholdMillisecondsForCapabilitiesApi);
int commandLineDelay = 0;
if (base::StringToInt(delayString, &commandLineDelay)) {
return base::Milliseconds(commandLineDelay);
}
}
return kShowSigninUpgradePromoMaxDelay;
}
bool ShouldPresentUserSigninUpgrade(ProfileIOS* profile,
const base::Version& current_version) {
DCHECK(profile);
DCHECK(current_version.IsValid());
if (tests_hook::DisableUpgradeSigninPromo()) {
return false;
}
if (profile->IsOffTheRecord()) {
return false;
}
// There will be an error shown if the user chooses to sign in or select
// another account while offline.
if (net::NetworkChangeNotifier::IsOffline()) {
return false;
}
// Sign-in can be disabled by policy or through user Settings.
AuthenticationService* authentication_service =
AuthenticationServiceFactory::GetForProfile(profile);
if (!authentication_service->SigninEnabled()) {
return false;
}
AuthenticationService* auth_service =
AuthenticationServiceFactory::GetForProfile(profile);
if (auth_service->HasPrimaryIdentity(signin::ConsentLevel::kSignin)) {
syncer::SyncService* sync_service =
SyncServiceFactory::GetForProfile(profile);
switch (history_sync::GetSkipReason(sync_service, auth_service,
profile->GetPrefs(), YES)) {
case history_sync::HistorySyncSkipReason::kNone:
// Need to show the upgrade promo, to show the history sync opt-in.
break;
case history_sync::HistorySyncSkipReason::kNotSignedIn:
NOTREACHED();
case history_sync::HistorySyncSkipReason::kAlreadyOptedIn:
case history_sync::HistorySyncSkipReason::kSyncForbiddenByPolicies:
case history_sync::HistorySyncSkipReason::kDeclinedTooOften:
return false;
}
}
// Avoid showing the upgrade sign-in promo when the device restore sign-in
// promo should be shown instead.
if (GetPreRestoreIdentity(profile->GetPrefs()).has_value()) {
return false;
}
// Don't show the promo if there are no identities. This should be tested
// before ForceStartupSigninPromo() to avoid any DCHECK failures if
// ForceStartupSigninPromo() returns true.
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
NSSet<NSString*>* identities_on_device_gaia_ids =
GaiaIdSetWithAccountInfos(identity_manager->GetAccountsOnDevice());
if (identities_on_device_gaia_ids.count == 0) {
return false;
}
// Used for testing purposes only.
if (signin::ForceStartupSigninPromo() ||
experimental_flags::AlwaysDisplayUpgradePromo()) {
return true;
}
PrefService* local_state = GetApplicationContext()->GetLocalState();
base::Time next_show_time = local_state->GetTime(prefs::kNextSSORecallTime);
// We just store the next show time for now to ramp up clients for the
// experiment later. See crbug.com/408962000.
if (next_show_time.is_null()) {
local_state->SetTime(prefs::kNextSSORecallTime,
base::Time::Now() + DurationBetweenPromoTriggers());
}
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
// Show the promo at most every two major versions.
NSString* version_string =
[defaults stringForKey:kDisplayedSSORecallForMajorVersionKey];
const base::Version version_shown(base::SysNSStringToUTF8(version_string));
// If the version was not set, we need to set it in order to wait 2 major
// releases to show the sign-in promo.
if (!version_shown.IsValid()) {
[defaults setObject:base::SysUTF8ToNSString(current_version.GetString())
forKey:kDisplayedSSORecallForMajorVersionKey];
return false;
}
// Wait 2 major releases to show the sign-in promo.
if (current_version.components()[0] - version_shown.components()[0] < 2) {
return false;
}
// TODO(crbug.com/416634715): Replace user defaults interaction count with FET
// event count. The sign-in promo should be shown twice, even if no account
// has been added.
NSInteger display_count =
[defaults integerForKey:kSigninPromoViewDisplayCountKey];
if (display_count <= 1) {
return true;
}
// Otherwise, it can be shown only if a new account has been added.
NSArray<NSString*>* last_known_gaia_id_list =
[defaults arrayForKey:kLastShownAccountGaiaIdVersionKey];
return IsStrictSubset(last_known_gaia_id_list, identities_on_device_gaia_ids);
}
bool ShouldPresentWebSignin(ProfileIOS* profile) {
AuthenticationService* authentication_service =
AuthenticationServiceFactory::GetForProfile(profile);
if (authentication_service->HasPrimaryIdentity(
signin::ConsentLevel::kSignin)) {
// For some reasons, Gaia might ask for the web sign-in while the user is
// already signed in. It might be a race conditions with a token already
// disabled on Gaia, and Chrome not aware of it yet?
// To avoid a crash (hitting CHECK() to sign-in while already being signed
// in), we need to skip the web sign-in dialog.
// Related to crbug.com/1308448.
RecordConsistencyPromoUserAction(
signin_metrics::AccountConsistencyPromoAction::
SUPPRESSED_ALREADY_SIGNED_IN,
signin_metrics::AccessPoint::kWebSignin);
return false;
}
signin_metrics::AccessPoint web_signin_access_point =
signin_metrics::AccessPoint::kWebSignin;
// Skip the bottom sheet sign-in dialog if the user cannot sign-in.
switch (authentication_service->GetServiceStatus()) {
case AuthenticationService::ServiceStatus::SigninForcedByPolicy:
case AuthenticationService::ServiceStatus::SigninDisabledByUser:
case AuthenticationService::ServiceStatus::SigninDisabledByPolicy:
case AuthenticationService::ServiceStatus::SigninDisabledByInternal:
RecordConsistencyPromoUserAction(
signin_metrics::AccountConsistencyPromoAction::
SUPPRESSED_SIGNIN_NOT_ALLOWED,
web_signin_access_point);
return false;
case AuthenticationService::ServiceStatus::SigninAllowed:
break;
}
// Show the sign-in dialog less than `kSigninWebSignDismissalCount` times.
PrefService* user_pref_service = profile->GetPrefs();
const int current_dismissal_count =
user_pref_service->GetInteger(prefs::kSigninWebSignDismissalCount);
if (current_dismissal_count >= kDefaultWebSignInDismissalCount) {
RecordConsistencyPromoUserAction(
signin_metrics::AccountConsistencyPromoAction::
SUPPRESSED_CONSECUTIVE_DISMISSALS,
web_signin_access_point);
return false;
}
return true;
}
void RecordUpgradePromoSigninStarted(
signin::IdentityManager* identity_manager,
ChromeAccountManagerService* account_manager_service,
const base::Version& current_version) {
DCHECK(identity_manager);
DCHECK(account_manager_service);
DCHECK(current_version.IsValid());
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
PrefService* local_state = GetApplicationContext()->GetLocalState();
local_state->SetTime(prefs::kNextSSORecallTime,
base::Time::Now() + DurationBetweenPromoTriggers());
[defaults setObject:base::SysUTF8ToNSString(current_version.GetString())
forKey:kDisplayedSSORecallForMajorVersionKey];
std::vector<AccountInfo> account_infos =
identity_manager->GetAccountsOnDevice();
NSSet<NSString*>* gaia_id_on_device_set =
GaiaIdSetWithAccountInfos(account_infos);
[defaults setObject:gaia_id_on_device_set.allObjects
forKey:kLastShownAccountGaiaIdVersionKey];
NSInteger display_count =
[defaults integerForKey:kSigninPromoViewDisplayCountKey];
++display_count;
[defaults setInteger:display_count forKey:kSigninPromoViewDisplayCountKey];
}
Tribool TriboolFromCapabilityResult(SystemIdentityCapabilityResult result) {
switch (result) {
case SystemIdentityCapabilityResult::kTrue:
return Tribool::kTrue;
case SystemIdentityCapabilityResult::kFalse:
return Tribool::kFalse;
case SystemIdentityCapabilityResult::kUnknown:
return Tribool::kUnknown;
}
NOTREACHED();
}
NSArray<id<SystemIdentity>>* GetIdentitiesOnDevice(
signin::IdentityManager* identityManager,
ChromeAccountManagerService* accountManagerService) {
std::vector<AccountInfo> accountInfos =
identityManager->GetAccountsOnDevice();
return accountManagerService->GetIdentitiesOnDeviceWithGaiaIDs(accountInfos);
}
NSArray<id<SystemIdentity>>* GetIdentitiesOnDevice(ProfileIOS* profile) {
return GetIdentitiesOnDevice(
IdentityManagerFactory::GetForProfile(profile),
ChromeAccountManagerServiceFactory::GetForProfile(profile));
}
id<SystemIdentity> GetDefaultIdentityOnDevice(
signin::IdentityManager* identityManager,
ChromeAccountManagerService* accountManagerService) {
NSArray<id<SystemIdentity>>* identitiesOnDevice =
GetIdentitiesOnDevice(identityManager, accountManagerService);
return [identitiesOnDevice firstObject];
}
id<SystemIdentity> GetDefaultIdentityOnDevice(ProfileIOS* profile) {
return GetDefaultIdentityOnDevice(
IdentityManagerFactory::GetForProfile(profile),
ChromeAccountManagerServiceFactory::GetForProfile(profile));
}
ProfileSignoutRequest::ProfileSignoutRequest(
signin_metrics::ProfileSignout source)
: source_(source),
prepare_callback_(base::DoNothing()),
completion_callback_(base::DoNothing()) {}
ProfileSignoutRequest::~ProfileSignoutRequest() {
CHECK(run_has_been_called_);
}
ProfileSignoutRequest&& ProfileSignoutRequest::SetSnackbarMessage(
SnackbarMessage* snackbar_message,
bool force_snackbar_over_toolbar) && {
CHECK(!run_has_been_called_);
snackbar_message_ = snackbar_message;
force_snackbar_over_toolbar_ = force_snackbar_over_toolbar;
return std::move(*this);
}
ProfileSignoutRequest&& ProfileSignoutRequest::SetPrepareCallback(
PrepareCallback prepare_callback) && {
CHECK(!run_has_been_called_);
CHECK(!prepare_callback.is_null());
prepare_callback_ = std::move(prepare_callback);
return std::move(*this);
}
ProfileSignoutRequest&& ProfileSignoutRequest::SetCompletionCallback(
CompletionCallback completion_callback) && {
CHECK(!run_has_been_called_);
CHECK(!completion_callback.is_null());
completion_callback_ = std::move(completion_callback);
return std::move(*this);
}
ProfileSignoutRequest&& ProfileSignoutRequest::SetShouldRecordMetrics(
bool value) && {
CHECK(!run_has_been_called_);
should_record_metrics_ = value;
return std::move(*this);
}
void ProfileSignoutRequest::Run(Browser* browser) && {
CHECK(!run_has_been_called_);
run_has_been_called_ = true;
// The regular browser should be used to execute the signout.
CHECK_EQ(browser->type(), Browser::Type::kRegular);
SceneState* scene_state = browser->GetSceneState();
ChangeProfileContinuation continuation =
CreateChangeProfileSignoutContinuation(
source_, force_snackbar_over_toolbar_, should_record_metrics_,
snackbar_message_, std::move(completion_callback_));
ProfileIOS* profile = browser->GetProfile();
AuthenticationService* authentication_service =
AuthenticationServiceFactory::GetForProfile(profile);
if (source_ == signin_metrics::ProfileSignout::kPrefChanged) {
ChangeProfileContinuation postSignoutContinuation =
CreateChangeProfileForceSignoutContinuation();
continuation = ChainChangeProfileContinuations(
std::move(continuation), std::move(postSignoutContinuation));
}
if (!ShouldSwitchProfileAtSignout(authentication_service, profile)) {
std::move(prepare_callback_).Run(/*will_change_profile=*/false);
std::move(continuation).Run(scene_state, base::DoNothing());
return;
}
if (source_ == signin_metrics::ProfileSignout::kUserClickedSignoutSettings) {
ChangeProfileContinuation postSignoutContinuation =
CreateChangeProfileSettingsContinuation();
continuation = ChainChangeProfileContinuations(
std::move(continuation), std::move(postSignoutContinuation));
}
std::move(prepare_callback_).Run(/*will_change_profile=*/true);
SwitchToPersonalProfile(scene_state,
ChangeProfileReason::kManagedAccountSignOut,
std::move(continuation));
}
void MultiProfileSignOutForProfile(
ProfileIOS* profile,
signin_metrics::ProfileSignout signout_source,
base::OnceClosure signout_completion_closure) {
// Simply sign out if no profile switching is needed.
AuthenticationService* authentication_service =
AuthenticationServiceFactory::GetForProfile(profile);
if (!ShouldSwitchProfileAtSignout(authentication_service, profile)) {
authentication_service->SignOut(
signout_source,
base::CallbackToBlock(std::move(signout_completion_closure)));
return;
}
// The API to change a profile work on a `SceneState`. Each `SceneState` has a
// regular, inactive and incognito browser associated.
// All three Browser points to the same `SceneState`, so this code only need
// to consider the regular Browser.
auto browser_list =
BrowserListFactory::GetForProfile(profile)->BrowsersOfType(
BrowserList::BrowserType::kRegular);
// Only call `signout_completion_closure` after all browsers have switched to
// the personal profile.
base::RepeatingClosure barrier = base::BarrierClosure(
browser_list.size(), std::move(signout_completion_closure));
// Sign the user out in all browsers.
for (Browser* browser : browser_list) {
ChangeProfileContinuation continuation =
CreateChangeProfileSignoutContinuation(
signout_source, /*force_snackbar_over_toolbar=*/false,
/*should_record_metrics=*/false, /*snackbar_message =*/nil,
base::IgnoreArgs<SceneState*>(barrier));
SwitchToPersonalProfile(browser->GetSceneState(),
ChangeProfileReason::kManagedAccountSignOut,
std::move(continuation));
}
}
bool IsFullscreenSigninPromoManagerMigrationDone() {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
return [defaults boolForKey:kFullscreenSigninPromoManagerMigrationDone];
}
void LogFullscreenSigninPromoManagerMigrationDone() {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:kFullscreenSigninPromoManagerMigrationDone];
}
void FetchUnsyncedDataForSignOutOrProfileSwitching(
syncer::SyncService* sync_service,
UnsyncedDataForSignoutOrProfileSwitchingCallback callback) {
constexpr syncer::DataTypeSet kDataTypesToQuery =
syncer::TypesRequiringUnsyncedDataCheckOnSignout();
sync_service->GetTypesWithUnsyncedData(
kDataTypesToQuery,
base::BindOnce(&DataCountsMapToDataTypeSet).Then(std::move(callback)));
}
// Post an asynchronous request to switch from a managed profile to the
// personal profile, running `continuation` when the change completes.
void SwitchToPersonalProfile(SceneState* scene_state,
ChangeProfileReason reason,
ChangeProfileContinuation continuation) {
ProfileManagerIOS* profile_manager =
GetApplicationContext()->GetProfileManager();
std::string personal_profile_name =
profile_manager->GetProfileAttributesStorage()->GetPersonalProfileName();
CHECK(profile_manager->HasProfileWithName(personal_profile_name));
SwitchToProfile(scene_state, personal_profile_name, reason,
std::move(continuation));
}
bool DifferentUserIsSignedInInAnotherScene(SceneState* scene_state) {
ProfileIOS* profile = scene_state.profileState.profile;
AppState* app_state = scene_state.profileState.appState;
for (ProfileState* profile_state in app_state.profileStates) {
if (profile == profile_state.profile) {
continue;
}
auto* identity_manager =
IdentityManagerFactory::GetForProfile(profile_state.profile);
if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
return true;
}
}
return false;
}
Browser* GetRegularBrowser(Browser* browser) {
if (browser->type() == Browser::Type::kRegular) {
// Returning the browser directly ensure that this work in test without
// scene state.
return browser;
}
return browser->GetSceneState()
.browserProviderInterface.mainBrowserProvider.browser;
}
} // namespace signin