blob: 981b3e41574df9a41c82ec9de2d3ad03cef61d2f [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/password_manager/core/browser/password_manager_features_util.h"
#include <algorithm>
#include "base/containers/flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/values.h"
#include "components/autofill/core/common/gaia_id_hash.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/sync/driver/sync_service.h"
#include "components/sync/driver/sync_user_settings.h"
#include "google_apis/gaia/gaia_urls.h"
using autofill::GaiaIdHash;
using password_manager::metrics_util::PasswordAccountStorageUsageLevel;
using password_manager::metrics_util::PasswordAccountStorageUserState;
namespace password_manager {
namespace features_util {
namespace {
// Returns whether the account-scoped password storage can be enabled in
// principle for the current profile. This is constant for a given profile
// (until browser restart).
bool CanAccountStorageBeEnabled(const syncer::SyncService* sync_service) {
if (!base::FeatureList::IsEnabled(features::kEnablePasswordsAccountStorage)) {
return false;
}
// |sync_service| is null in incognito mode, or if --disable-sync was
// specified on the command-line.
if (!sync_service)
return false;
return true;
}
// Whether the currently signed-in user (if any) is eligible for using the
// account-scoped password storage. This is the case if:
// - The account storage can be enabled in principle.
// - Sync-the-transport is running (i.e. there's a signed-in user, Sync is not
// disabled by policy, etc).
// - There is no custom passphrase (because Sync transport offers no way to
// enter the passphrase yet). Note that checking this requires the SyncEngine
// to be initialized.
// - Sync-the-feature is NOT enabled (if it is, there's only a single combined
// storage).
bool IsUserEligibleForAccountStorage(const syncer::SyncService* sync_service) {
return CanAccountStorageBeEnabled(sync_service) &&
sync_service->IsEngineInitialized() &&
!sync_service->GetUserSettings()->IsUsingSecondaryPassphrase() &&
!sync_service->IsSyncFeatureEnabled();
}
PasswordForm::Store PasswordStoreFromInt(int value) {
switch (value) {
case static_cast<int>(PasswordForm::Store::kProfileStore):
return PasswordForm::Store::kProfileStore;
case static_cast<int>(PasswordForm::Store::kAccountStore):
return PasswordForm::Store::kAccountStore;
}
return PasswordForm::Store::kNotSet;
}
const char kAccountStorageOptedInKey[] = "opted_in";
const char kAccountStorageDefaultStoreKey[] = "default_store";
const char kMoveToAccountStoreOfferedCountKey[] =
"move_to_account_store_refused_count";
// Returns the total number of accounts for which an opt-in to the account
// storage exists. Used for metrics.
int GetNumberOfOptedInAccounts(const PrefService* pref_service) {
const base::DictionaryValue* global_pref =
pref_service->GetDictionary(prefs::kAccountStoragePerAccountSettings);
int count = 0;
for (const std::pair<std::string, std::unique_ptr<base::Value>>& entry :
*global_pref) {
if (entry.second->FindBoolKey(kAccountStorageOptedInKey).value_or(false))
++count;
}
return count;
}
// Helper class for reading account storage settings for a given account.
class AccountStorageSettingsReader {
public:
AccountStorageSettingsReader(const PrefService* prefs,
const GaiaIdHash& gaia_id_hash) {
const base::DictionaryValue* global_pref =
prefs->GetDictionary(prefs::kAccountStoragePerAccountSettings);
if (global_pref)
account_settings_ = global_pref->FindDictKey(gaia_id_hash.ToBase64());
}
bool IsOptedIn() {
if (!account_settings_)
return false;
return account_settings_->FindBoolKey(kAccountStorageOptedInKey)
.value_or(false);
}
PasswordForm::Store GetDefaultStore() const {
if (!account_settings_)
return PasswordForm::Store::kNotSet;
base::Optional<int> value =
account_settings_->FindIntKey(kAccountStorageDefaultStoreKey);
if (!value)
return PasswordForm::Store::kNotSet;
return PasswordStoreFromInt(*value);
}
int GetMoveOfferedToNonOptedInUserCount() const {
if (!account_settings_)
return 0;
return account_settings_->FindIntKey(kMoveToAccountStoreOfferedCountKey)
.value_or(0);
}
private:
// May be null, if no settings for this account were saved yet.
const base::Value* account_settings_ = nullptr;
};
// Helper class for updating account storage settings for a given account. Like
// with DictionaryPrefUpdate, updates are only published once the instance gets
// destroyed.
class ScopedAccountStorageSettingsUpdate {
public:
ScopedAccountStorageSettingsUpdate(PrefService* prefs,
const GaiaIdHash& gaia_id_hash)
: update_(prefs, prefs::kAccountStoragePerAccountSettings),
account_hash_(gaia_id_hash.ToBase64()) {}
base::Value* GetOrCreateAccountSettings() {
base::Value* account_settings = update_->FindDictKey(account_hash_);
if (!account_settings) {
account_settings =
update_->SetKey(account_hash_, base::DictionaryValue());
}
DCHECK(account_settings);
return account_settings;
}
void SetOptedIn() {
base::Value* account_settings = GetOrCreateAccountSettings();
// The count of refusals is only tracked when the user is not opted-in.
account_settings->RemoveKey(kMoveToAccountStoreOfferedCountKey);
account_settings->SetBoolKey(kAccountStorageOptedInKey, true);
}
void SetDefaultStore(PasswordForm::Store default_store) {
base::Value* account_settings = GetOrCreateAccountSettings();
account_settings->SetIntKey(kAccountStorageDefaultStoreKey,
static_cast<int>(default_store));
}
void RecordMoveOfferedToNonOptedInUser() {
base::Value* account_settings = GetOrCreateAccountSettings();
int count = account_settings->FindIntKey(kMoveToAccountStoreOfferedCountKey)
.value_or(0);
account_settings->SetIntKey(kMoveToAccountStoreOfferedCountKey, ++count);
}
void ClearAllSettings() { update_->RemoveKey(account_hash_); }
private:
DictionaryPrefUpdate update_;
const std::string account_hash_;
};
} // namespace
bool IsOptedInForAccountStorage(const PrefService* pref_service,
const syncer::SyncService* sync_service) {
DCHECK(pref_service);
// If the account storage can't be enabled (e.g. because the feature flag was
// turned off), then don't consider the user opted in, even if the pref is
// set.
// Note: IsUserEligibleForAccountStorage() is not appropriate here, because
// a) Sync-the-feature users are not considered eligible, but might have
// opted in before turning on Sync, and
// b) eligibility requires IsEngineInitialized() (i.e. will be false for a
// few seconds after browser startup).
if (!CanAccountStorageBeEnabled(sync_service))
return false;
// The opt-in is per account, so if there's no account then there's no opt-in.
std::string gaia_id = sync_service->GetAuthenticatedAccountInfo().gaia;
if (gaia_id.empty())
return false;
return AccountStorageSettingsReader(pref_service,
GaiaIdHash::FromGaiaId(gaia_id))
.IsOptedIn();
}
bool ShouldShowAccountStorageReSignin(const PrefService* pref_service,
const syncer::SyncService* sync_service,
const GURL& current_page_url) {
DCHECK(pref_service);
// Checks that the sync_service is not null and the feature is enabled.
if (!CanAccountStorageBeEnabled(sync_service)) {
return false; // Opt-in wouldn't work here, so don't show the re-signin.
}
// In order to show a re-signin prompt, no user may be logged in.
if (!sync_service->HasDisableReason(
syncer::SyncService::DisableReason::DISABLE_REASON_NOT_SIGNED_IN)) {
return false;
}
if (current_page_url.GetOrigin() ==
GaiaUrls::GetInstance()->gaia_url().GetOrigin()) {
return false;
}
// Show the opt-in if any known previous user opted into using the account
// storage before and might want to access it again.
return base::ranges::any_of(
*pref_service->GetDictionary(prefs::kAccountStoragePerAccountSettings),
[](const std::pair<std::string, std::unique_ptr<base::Value>>& p) {
return p.second->FindBoolKey(kAccountStorageOptedInKey).value_or(false);
});
}
bool ShouldShowAccountStorageOptIn(const PrefService* pref_service,
const syncer::SyncService* sync_service) {
DCHECK(pref_service);
// Show the opt-in if the user is eligible, but not yet opted in.
return IsUserEligibleForAccountStorage(sync_service) &&
!IsOptedInForAccountStorage(pref_service, sync_service) &&
!sync_service->IsSyncFeatureEnabled();
}
void OptInToAccountStorage(PrefService* pref_service,
const syncer::SyncService* sync_service) {
DCHECK(pref_service);
DCHECK(sync_service);
DCHECK(
base::FeatureList::IsEnabled(features::kEnablePasswordsAccountStorage));
std::string gaia_id = sync_service->GetAuthenticatedAccountInfo().gaia;
if (gaia_id.empty()) {
// Maybe the account went away since the opt-in UI was shown. This should be
// rare, but is ultimately harmless - just do nothing here.
return;
}
ScopedAccountStorageSettingsUpdate(pref_service,
GaiaIdHash::FromGaiaId(gaia_id))
.SetOptedIn();
// Record the total number of (now) opted-in accounts.
base::UmaHistogramExactLinear(
"PasswordManager.AccountStorage.NumOptedInAccountsAfterOptIn",
GetNumberOfOptedInAccounts(pref_service), 10);
}
void OptOutOfAccountStorageAndClearSettings(
PrefService* pref_service,
const syncer::SyncService* sync_service) {
DCHECK(pref_service);
DCHECK(sync_service);
DCHECK(
base::FeatureList::IsEnabled(features::kEnablePasswordsAccountStorage));
std::string gaia_id = sync_service->GetAuthenticatedAccountInfo().gaia;
bool account_exists = !gaia_id.empty();
base::UmaHistogramBoolean(
"PasswordManager.AccountStorage.SignedInAccountFoundDuringOptOut",
account_exists);
if (!account_exists) {
// In rare cases, it could happen that the account went away since the
// opt-out UI was triggered.
return;
}
OptOutOfAccountStorageAndClearSettingsForAccount(pref_service, gaia_id);
}
void OptOutOfAccountStorageAndClearSettingsForAccount(
PrefService* pref_service,
const std::string& gaia_id) {
ScopedAccountStorageSettingsUpdate(pref_service,
GaiaIdHash::FromGaiaId(gaia_id))
.ClearAllSettings();
// Record the total number of (still) opted-in accounts.
base::UmaHistogramExactLinear(
"PasswordManager.AccountStorage.NumOptedInAccountsAfterOptOut",
GetNumberOfOptedInAccounts(pref_service), 10);
}
bool ShouldShowAccountStorageBubbleUi(const PrefService* pref_service,
const syncer::SyncService* sync_service) {
// `sync_service` is null in incognito mode, or if --disable-sync was
// specified on the command-line.
return sync_service && !sync_service->IsSyncFeatureEnabled() &&
(IsOptedInForAccountStorage(pref_service, sync_service) ||
IsUserEligibleForAccountStorage(sync_service));
}
PasswordForm::Store GetDefaultPasswordStore(
const PrefService* pref_service,
const syncer::SyncService* sync_service) {
DCHECK(pref_service);
if (!IsUserEligibleForAccountStorage(sync_service))
return PasswordForm::Store::kProfileStore;
std::string gaia_id = sync_service->GetAuthenticatedAccountInfo().gaia;
if (gaia_id.empty())
return PasswordForm::Store::kProfileStore;
PasswordForm::Store default_store =
AccountStorageSettingsReader(pref_service,
GaiaIdHash::FromGaiaId(gaia_id))
.GetDefaultStore();
// If none of the early-outs above triggered, then we *can* save to the
// account store in principle (though the user might not have opted in to that
// yet).
if (default_store == PasswordForm::Store::kNotSet) {
// If the user hasn't made a choice about the default store yet, retrieve it
// from a feature param.
bool save_to_profile_store = base::GetFieldTrialParamByFeatureAsBool(
features::kEnablePasswordsAccountStorage,
features::kSaveToProfileStoreByDefault,
features::kSaveToProfileStoreByDefaultDefaultValue);
return save_to_profile_store ? PasswordForm::Store::kProfileStore
: PasswordForm::Store::kAccountStore;
}
return default_store;
}
void SetDefaultPasswordStore(PrefService* pref_service,
const syncer::SyncService* sync_service,
PasswordForm::Store default_store) {
DCHECK(pref_service);
DCHECK(sync_service);
DCHECK(
base::FeatureList::IsEnabled(features::kEnablePasswordsAccountStorage));
std::string gaia_id = sync_service->GetAuthenticatedAccountInfo().gaia;
if (gaia_id.empty()) {
// Maybe the account went away since the UI was shown. This should be rare,
// but is ultimately harmless - just do nothing here.
return;
}
ScopedAccountStorageSettingsUpdate(pref_service,
GaiaIdHash::FromGaiaId(gaia_id))
.SetDefaultStore(default_store);
base::UmaHistogramEnumeration("PasswordManager.DefaultPasswordStoreSet",
default_store);
}
void KeepAccountStorageSettingsOnlyForUsers(
PrefService* pref_service,
const std::vector<std::string>& gaia_ids) {
DCHECK(pref_service);
// Build a set of hashes of all the Gaia IDs.
std::vector<std::string> hashes_to_keep_list;
for (const std::string& gaia_id : gaia_ids)
hashes_to_keep_list.push_back(GaiaIdHash::FromGaiaId(gaia_id).ToBase64());
base::flat_set<std::string> hashes_to_keep(std::move(hashes_to_keep_list));
// Now remove any settings for account that are *not* in the set of hashes.
// DictionaryValue doesn't allow removing elements while iterating, so first
// collect all the keys to remove, then actually remove them in a second pass.
DictionaryPrefUpdate update(pref_service,
prefs::kAccountStoragePerAccountSettings);
std::vector<std::string> keys_to_remove;
for (auto kv : update->DictItems()) {
if (!hashes_to_keep.contains(kv.first))
keys_to_remove.push_back(kv.first);
}
for (const std::string& key_to_remove : keys_to_remove)
update->RemoveKey(key_to_remove);
}
void ClearAccountStorageSettingsForAllUsers(PrefService* pref_service) {
DCHECK(pref_service);
// Record the total number of opted-in accounts before clearing them.
base::UmaHistogramExactLinear(
"PasswordManager.AccountStorage.ClearedOptInForAllAccounts",
GetNumberOfOptedInAccounts(pref_service), 10);
pref_service->ClearPref(prefs::kAccountStoragePerAccountSettings);
}
PasswordAccountStorageUserState ComputePasswordAccountStorageUserState(
const PrefService* pref_service,
const syncer::SyncService* sync_service) {
DCHECK(pref_service);
// The SyncService can be null in incognito, or due to a commandline flag. In
// those cases, simply consider the user as signed out.
if (!sync_service)
return PasswordAccountStorageUserState::kSignedOutUser;
if (sync_service->IsSyncFeatureEnabled())
return PasswordAccountStorageUserState::kSyncUser;
if (sync_service->HasDisableReason(
syncer::SyncService::DisableReason::DISABLE_REASON_NOT_SIGNED_IN)) {
// Signed out. Check if any account storage opt-in exists.
return ShouldShowAccountStorageReSignin(pref_service, sync_service, GURL())
? PasswordAccountStorageUserState::kSignedOutAccountStoreUser
: PasswordAccountStorageUserState::kSignedOutUser;
}
bool saving_locally = GetDefaultPasswordStore(pref_service, sync_service) ==
PasswordForm::Store::kProfileStore;
// Signed in. Check for account storage opt-in.
if (IsOptedInForAccountStorage(pref_service, sync_service)) {
// Signed in and opted in. Check default storage location.
return saving_locally
? PasswordAccountStorageUserState::
kSignedInAccountStoreUserSavingLocally
: PasswordAccountStorageUserState::kSignedInAccountStoreUser;
}
// Signed in but not opted in. Check default storage location.
return saving_locally
? PasswordAccountStorageUserState::kSignedInUserSavingLocally
: PasswordAccountStorageUserState::kSignedInUser;
}
PasswordAccountStorageUsageLevel ComputePasswordAccountStorageUsageLevel(
const PrefService* pref_service,
const syncer::SyncService* sync_service) {
using UserState = PasswordAccountStorageUserState;
using UsageLevel = PasswordAccountStorageUsageLevel;
switch (ComputePasswordAccountStorageUserState(pref_service, sync_service)) {
case UserState::kSignedOutUser:
case UserState::kSignedOutAccountStoreUser:
case UserState::kSignedInUser:
case UserState::kSignedInUserSavingLocally:
return UsageLevel::kNotUsingAccountStorage;
case UserState::kSignedInAccountStoreUser:
case UserState::kSignedInAccountStoreUserSavingLocally:
return UsageLevel::kUsingAccountStorage;
case UserState::kSyncUser:
return UsageLevel::kSyncing;
}
}
void RecordMoveOfferedToNonOptedInUser(
PrefService* pref_service,
const syncer::SyncService* sync_service) {
DCHECK(pref_service);
DCHECK(sync_service);
std::string gaia_id = sync_service->GetAuthenticatedAccountInfo().gaia;
DCHECK(!gaia_id.empty());
DCHECK(!AccountStorageSettingsReader(pref_service,
GaiaIdHash::FromGaiaId(gaia_id))
.IsOptedIn());
ScopedAccountStorageSettingsUpdate(pref_service,
GaiaIdHash::FromGaiaId(gaia_id))
.RecordMoveOfferedToNonOptedInUser();
}
int GetMoveOfferedToNonOptedInUserCount(
const PrefService* pref_service,
const syncer::SyncService* sync_service) {
DCHECK(pref_service);
DCHECK(sync_service);
std::string gaia_id = sync_service->GetAuthenticatedAccountInfo().gaia;
DCHECK(!gaia_id.empty());
AccountStorageSettingsReader reader(pref_service,
GaiaIdHash::FromGaiaId(gaia_id));
DCHECK(!reader.IsOptedIn());
return reader.GetMoveOfferedToNonOptedInUserCount();
}
} // namespace features_util
} // namespace password_manager