Commit 50480d41 authored by Marc Treib's avatar Marc Treib Committed by Commit Bot

MultiStorePasswordSaveManager: Track action for each store

This CL adds profile_store_action_ and account_store_action_ to
MultiStorePasswordSaveManager (in addition to pending_credentials_state_
from the base class). Explicitly tracking the two states, instead of
just a single "canonical" one, allows us to be more explicit in how
conflicts are resolved, and in particular it allows us to handle some
edge cases that we couldn't handle correctly before.
It also adds an integration test for one such edge case.

Bug: 1012203, 1060524
Change-Id: I9602c3641da761a76b2dc9a1f62b630ef2fbcb2e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2139654
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Auto-Submit: Marc Treib <treib@chromium.org>
Reviewed-by: default avatarMohamed Amir Yosef <mamir@chromium.org>
Cr-Commit-Position: refs/heads/master@{#758777}
parent 40f67347
......@@ -541,6 +541,36 @@ IN_PROC_BROWSER_TEST_F(PasswordManagerSyncTest,
ASSERT_THAT(GetAllLoginsFromAccountPasswordStore(),
ElementsAre(MatchesLogin("user", "pass")));
}
IN_PROC_BROWSER_TEST_F(PasswordManagerSyncTest,
AutoUpdatePSLMatchInBothStoresOnSuccessfulUse) {
ASSERT_TRUE(SetupClients()) << "SetupClients() failed.";
// Add the same PSL-matched credential to both stores (i.e. it's stored for
// psl.example.com instead of www.example.com).
AddCredentialToFakeServer(CreateTestPSLPasswordForm("user", "pass"));
AddLocalCredential(CreateTestPSLPasswordForm("user", "pass"));
SetupSyncTransportWithPasswordAccountStorage();
content::WebContents* web_contents = nullptr;
GetNewTab(GetBrowser(0), &web_contents);
// Go to a form (on www.) and submit it with the saved credentials.
NavigateToFile(web_contents, "/password/simple_password.html");
FillAndSubmitPasswordForm(web_contents, "user", "pass");
// Now the PSL-matched credential should have been automatically saved for
// www. as well, in both stores.
EXPECT_THAT(GetAllLoginsFromAccountPasswordStore(),
UnorderedElementsAre(
MatchesLoginAndRealm("user", "pass", GetWWWOrigin()),
MatchesLoginAndRealm("user", "pass", GetPSLOrigin())));
EXPECT_THAT(GetAllLoginsFromProfilePasswordStore(),
UnorderedElementsAre(
MatchesLoginAndRealm("user", "pass", GetWWWOrigin()),
MatchesLoginAndRealm("user", "pass", GetPSLOrigin())));
}
#endif // !defined(OS_CHROMEOS)
} // namespace
......@@ -54,27 +54,23 @@ bool AccountStoreMatchesContainForm(
PendingCredentialsState ResolvePendingCredentialsStates(
PendingCredentialsState profile_state,
PendingCredentialsState account_state) {
// Resolve the two states to a single canonical one, according to the
// following hierarchy:
// The result of this resolution will be used to decide whether to show a
// save or update prompt to the user. Resolve the two states to a single
// "canonical" one according to the following hierarchy:
// AUTOMATIC_SAVE > EQUAL_TO_SAVED_MATCH > UPDATE > NEW_LOGIN
// Note that UPDATE and NEW_LOGIN will result in an Update or Save bubble to
// Note that UPDATE or NEW_LOGIN will result in an Update or Save bubble to
// be shown, while AUTOMATIC_SAVE and EQUAL_TO_SAVED_MATCH will result in a
// silent save/update.
// Some interesting cases:
// NEW_LOGIN means that store doesn't know about the credential yet. If the
// other store knows anything at all, then that always wins.
// EQUAL_TO_SAVED_MATCH vs UPDATE: This means one store had a match, the other
// had a mismatch (same username but different password). We want to silently
// update the mismatch, which EQUAL achieves (since it'll still result in an
// update to date_last_used, and updates always go to both stores).
// TODO(crbug.com/1012203): AUTOMATIC_SAVE vs EQUAL: We should still perform
// the silent update for EQUAL (so last_use_date gets updated).
// TODO(crbug.com/1012203): AUTOMATIC_SAVE vs UPDATE: What's the expected
// outcome? Currently we'll auto-save the PSL match and ignore the update
// (which isn't too bad, since on the next submission the update will become
// a silent update through EQUAL_TO_SAVED_MATCH).
// TODO(crbug.com/1012203): AUTOMATIC_SAVE vs AUTOMATIC_SAVE: Somehow make
// sure that the save goes to both stores.
// had a mismatch (same username but different password). The mismatch should
// be updated silently, so resolve to EQUAL so that there's no visible prompt.
// AUTOMATIC_SAVE vs EQUAL_TO_SAVED_MATCH: These are both silent, so it
// doesn't really matter to which one we resolve.
// AUTOMATIC_SAVE vs UPDATE: Similar to EQUAL_TO_SAVED_MATCH vs UPDATE, the
// mismatch should be silently updated.
if (profile_state == PendingCredentialsState::AUTOMATIC_SAVE ||
account_state == PendingCredentialsState::AUTOMATIC_SAVE) {
return PendingCredentialsState::AUTOMATIC_SAVE;
......@@ -101,60 +97,91 @@ MultiStorePasswordSaveManager::MultiStorePasswordSaveManager(
std::unique_ptr<FormSaver> profile_form_saver,
std::unique_ptr<FormSaver> account_form_saver)
: PasswordSaveManagerImpl(std::move(profile_form_saver)),
account_store_form_saver_(std::move(account_form_saver)) {}
account_store_form_saver_(std::move(account_form_saver)) {
DCHECK(account_store_form_saver_);
}
MultiStorePasswordSaveManager::~MultiStorePasswordSaveManager() = default;
void MultiStorePasswordSaveManager::SaveInternal(
const std::vector<const PasswordForm*>& matches,
const base::string16& old_password) {
// For New Credentials, we should respect the default password store selected
// by user. In other cases such PSL matching, we respect the store in the
// retrieved credentials.
if (pending_credentials_state_ == PendingCredentialsState::NEW_LOGIN) {
pending_credentials_.in_store =
client_->GetPasswordFeatureManager()->GetDefaultPasswordStore();
void MultiStorePasswordSaveManager::SavePendingToStoreImpl(
const PasswordForm& parsed_submitted_form) {
auto matches = form_fetcher_->GetAllRelevantMatches();
PendingCredentialsStates states =
ComputePendingCredentialsStates(parsed_submitted_form, matches);
auto account_matches = AccountStoreMatches(matches);
auto profile_matches = ProfileStoreMatches(matches);
base::string16 old_account_password =
states.similar_saved_form_from_account_store
? states.similar_saved_form_from_account_store->password_value
: base::string16();
base::string16 old_profile_password =
states.similar_saved_form_from_profile_store
? states.similar_saved_form_from_profile_store->password_value
: base::string16();
if (states.profile_store_state == PendingCredentialsState::NEW_LOGIN &&
states.account_store_state == PendingCredentialsState::NEW_LOGIN) {
// If the credential is new to both stores, store it only in the default
// store.
if (AccountStoreIsDefault()) {
// TODO(crbug.com/1012203): Record UMA for how many passwords get dropped
// here. In rare cases it could happen that the user *was* opted in when
// the save dialog was shown, but now isn't anymore.
if (IsOptedInForAccountStorage()) {
account_store_form_saver_->Save(pending_credentials_, account_matches,
old_account_password);
}
} else {
form_saver_->Save(pending_credentials_, profile_matches,
old_profile_password);
}
return;
}
switch (pending_credentials_.in_store) {
case PasswordForm::Store::kAccountStore:
if (account_store_form_saver_ && IsOptedInForAccountStorage()) {
account_store_form_saver_->Save(
pending_credentials_, AccountStoreMatches(matches), old_password);
switch (states.profile_store_state) {
case PendingCredentialsState::AUTOMATIC_SAVE:
form_saver_->Save(pending_credentials_, profile_matches,
old_profile_password);
break;
case PendingCredentialsState::UPDATE:
case PendingCredentialsState::EQUAL_TO_SAVED_MATCH:
form_saver_->Update(pending_credentials_, profile_matches,
old_profile_password);
break;
// The NEW_LOGIN case was already handled separately above.
case PendingCredentialsState::NEW_LOGIN:
case PendingCredentialsState::NONE:
break;
}
// TODO(crbug.com/1012203): Record UMA for how many passwords get dropped
// here. In rare cases it could happen that the user *was* opted in when
// the save dialog was shown, but now isn't anymore.
if (IsOptedInForAccountStorage()) {
switch (states.account_store_state) {
case PendingCredentialsState::AUTOMATIC_SAVE:
account_store_form_saver_->Save(pending_credentials_, account_matches,
old_account_password);
break;
case PasswordForm::Store::kProfileStore:
form_saver_->Save(pending_credentials_, ProfileStoreMatches(matches),
old_password);
case PendingCredentialsState::UPDATE:
case PendingCredentialsState::EQUAL_TO_SAVED_MATCH:
account_store_form_saver_->Update(pending_credentials_, account_matches,
old_account_password);
break;
case PasswordForm::Store::kNotSet:
NOTREACHED();
// The NEW_LOGIN case was already handled separately above.
case PendingCredentialsState::NEW_LOGIN:
case PendingCredentialsState::NONE:
break;
}
}
void MultiStorePasswordSaveManager::UpdateInternal(
const std::vector<const PasswordForm*>& matches,
const base::string16& old_password) {
// Try to update both stores anyway because if credentials don't exist, the
// update operation is no-op.
form_saver_->Update(pending_credentials_, ProfileStoreMatches(matches),
old_password);
if (account_store_form_saver_ && IsOptedInForAccountStorage()) {
account_store_form_saver_->Update(
pending_credentials_, AccountStoreMatches(matches), old_password);
}
}
void MultiStorePasswordSaveManager::PermanentlyBlacklist(
const PasswordStore::FormDigest& form_digest) {
DCHECK(!client_->IsIncognito());
if (account_store_form_saver_ && IsOptedInForAccountStorage() &&
client_->GetPasswordFeatureManager()->GetDefaultPasswordStore() ==
PasswordForm::Store::kAccountStore) {
if (IsOptedInForAccountStorage() && AccountStoreIsDefault()) {
account_store_form_saver_->PermanentlyBlacklist(form_digest);
} else {
// For users who aren't yet opted-in to the account storage, we store their
......@@ -168,9 +195,8 @@ void MultiStorePasswordSaveManager::Unblacklist(
// Try to unblacklist in both stores anyway because if credentials don't
// exist, the unblacklist operation is no-op.
form_saver_->Unblacklist(form_digest);
if (account_store_form_saver_ && IsOptedInForAccountStorage()) {
if (IsOptedInForAccountStorage())
account_store_form_saver_->Unblacklist(form_digest);
}
}
std::unique_ptr<PasswordSaveManager> MultiStorePasswordSaveManager::Clone() {
......@@ -218,61 +244,78 @@ void MultiStorePasswordSaveManager::MoveCredentialsToAccountStore() {
}
}
std::pair<const autofill::PasswordForm*, PendingCredentialsState>
std::pair<const PasswordForm*, PendingCredentialsState>
MultiStorePasswordSaveManager::FindSimilarSavedFormAndComputeState(
const PasswordForm& parsed_submitted_form) const {
const std::vector<const PasswordForm*> matches =
form_fetcher_->GetBestMatches();
const PasswordForm* similar_saved_form_from_profile_store =
password_manager_util::GetMatchForUpdating(parsed_submitted_form,
ProfileStoreMatches(matches));
const PasswordForm* similar_saved_form_from_account_store =
password_manager_util::GetMatchForUpdating(parsed_submitted_form,
AccountStoreMatches(matches));
PendingCredentialsStates states = ComputePendingCredentialsStates(
parsed_submitted_form, form_fetcher_->GetBestMatches());
// Compute the PendingCredentialsState (i.e. what to do - save, update, silent
// update) separately for the two stores.
PendingCredentialsState profile_state = ComputePendingCredentialsState(
parsed_submitted_form, similar_saved_form_from_profile_store);
PendingCredentialsState account_state = ComputePendingCredentialsState(
parsed_submitted_form, similar_saved_form_from_account_store);
// Resolve the two states to a single canonical one.
PendingCredentialsState state =
ResolvePendingCredentialsStates(profile_state, account_state);
// Resolve the two states to a single canonical one. This will be used to
// decide what UI bubble (if any) to show to the user.
PendingCredentialsState resolved_state = ResolvePendingCredentialsStates(
states.profile_store_state, states.account_store_state);
// Choose which of the saved forms (if any) to use as the base for updating,
// based on which of the two states won the resolution.
// Note that if we got the same state for both stores, then it doesn't really
// matter which one we pick for updating, since the result will be the same
// anyway.
const PasswordForm* similar_saved_form = nullptr;
if (state == profile_state)
similar_saved_form = similar_saved_form_from_profile_store;
else if (state == account_state)
similar_saved_form = similar_saved_form_from_account_store;
const PasswordForm* resolved_similar_saved_form = nullptr;
if (resolved_state == states.profile_store_state)
resolved_similar_saved_form = states.similar_saved_form_from_profile_store;
else if (resolved_state == states.account_store_state)
resolved_similar_saved_form = states.similar_saved_form_from_account_store;
return std::make_pair(similar_saved_form, state);
return std::make_pair(resolved_similar_saved_form, resolved_state);
}
FormSaver* MultiStorePasswordSaveManager::GetFormSaverForGeneration() {
return IsOptedInForAccountStorage() && account_store_form_saver_
? account_store_form_saver_.get()
return IsOptedInForAccountStorage() ? account_store_form_saver_.get()
: form_saver_.get();
}
std::vector<const autofill::PasswordForm*>
std::vector<const PasswordForm*>
MultiStorePasswordSaveManager::GetRelevantMatchesForGeneration(
const std::vector<const autofill::PasswordForm*>& matches) {
const std::vector<const PasswordForm*>& matches) {
// For account store users, only matches in the account store should be
// considered for conflict resolution during generation.
return IsOptedInForAccountStorage() && account_store_form_saver_
return IsOptedInForAccountStorage()
? MatchesInStore(matches, PasswordForm::Store::kAccountStore)
: matches;
}
bool MultiStorePasswordSaveManager::IsOptedInForAccountStorage() {
// static
MultiStorePasswordSaveManager::PendingCredentialsStates
MultiStorePasswordSaveManager::ComputePendingCredentialsStates(
const PasswordForm& parsed_submitted_form,
const std::vector<const PasswordForm*>& matches) {
PendingCredentialsStates result;
// Try to find a similar existing saved form from each of the stores.
result.similar_saved_form_from_profile_store =
password_manager_util::GetMatchForUpdating(parsed_submitted_form,
ProfileStoreMatches(matches));
result.similar_saved_form_from_account_store =
password_manager_util::GetMatchForUpdating(parsed_submitted_form,
AccountStoreMatches(matches));
// Compute the PendingCredentialsState (i.e. what to do - save, update, silent
// update) separately for the two stores.
result.profile_store_state = ComputePendingCredentialsState(
parsed_submitted_form, result.similar_saved_form_from_profile_store);
result.account_store_state = ComputePendingCredentialsState(
parsed_submitted_form, result.similar_saved_form_from_account_store);
return result;
}
bool MultiStorePasswordSaveManager::IsOptedInForAccountStorage() const {
return client_->GetPasswordFeatureManager()->IsOptedInForAccountStorage();
}
bool MultiStorePasswordSaveManager::AccountStoreIsDefault() const {
return client_->GetPasswordFeatureManager()->GetDefaultPasswordStore() ==
PasswordForm::Store::kAccountStore;
}
} // namespace password_manager
......@@ -27,12 +27,6 @@ class MultiStorePasswordSaveManager : public PasswordSaveManagerImpl {
std::unique_ptr<FormSaver> account_form_saver);
~MultiStorePasswordSaveManager() override;
void SaveInternal(const std::vector<const autofill::PasswordForm*>& matches,
const base::string16& old_password) override;
void UpdateInternal(const std::vector<const autofill::PasswordForm*>& matches,
const base::string16& old_password) override;
void PermanentlyBlacklist(
const PasswordStore::FormDigest& form_digest) override;
void Unblacklist(const PasswordStore::FormDigest& form_digest) override;
......@@ -42,6 +36,8 @@ class MultiStorePasswordSaveManager : public PasswordSaveManagerImpl {
void MoveCredentialsToAccountStore() override;
protected:
void SavePendingToStoreImpl(
const autofill::PasswordForm& parsed_submitted_form) override;
std::pair<const autofill::PasswordForm*, PendingCredentialsState>
FindSimilarSavedFormAndComputeState(
const autofill::PasswordForm& parsed_submitted_form) const override;
......@@ -50,9 +46,24 @@ class MultiStorePasswordSaveManager : public PasswordSaveManagerImpl {
const std::vector<const autofill::PasswordForm*>& matches) override;
private:
bool IsOptedInForAccountStorage();
struct PendingCredentialsStates {
PendingCredentialsState profile_store_state = PendingCredentialsState::NONE;
PendingCredentialsState account_store_state = PendingCredentialsState::NONE;
const autofill::PasswordForm* similar_saved_form_from_profile_store =
nullptr;
const autofill::PasswordForm* similar_saved_form_from_account_store =
nullptr;
};
static PendingCredentialsStates ComputePendingCredentialsStates(
const autofill::PasswordForm& parsed_submitted_form,
const std::vector<const autofill::PasswordForm*>& matches);
bool IsOptedInForAccountStorage() const;
bool AccountStoreIsDefault() const;
const std::unique_ptr<FormSaver> account_store_form_saver_;
DISALLOW_COPY_AND_ASSIGN(MultiStorePasswordSaveManager);
};
......
......@@ -298,8 +298,7 @@ TEST_F(MultiStorePasswordSaveManagerTest, SaveInProfileStore) {
password_save_manager()->Save(observed_form_, parsed_submitted_form);
}
TEST_F(MultiStorePasswordSaveManagerTest,
UpdateBothStoresIfCredentialsExistInAccountStoreOnly) {
TEST_F(MultiStorePasswordSaveManagerTest, UpdateInAccountStoreOnly) {
SetAccountStoreEnabled(/*is_enabled=*/true);
PasswordForm saved_match_in_account_store(saved_match_);
......@@ -317,14 +316,13 @@ TEST_F(MultiStorePasswordSaveManagerTest,
// An update prompt should be shown.
EXPECT_TRUE(password_save_manager()->IsPasswordUpdate());
EXPECT_CALL(*mock_profile_form_saver(), Update(_, _, _));
EXPECT_CALL(*mock_profile_form_saver(), Update(_, _, _)).Times(0);
EXPECT_CALL(*mock_account_form_saver(), Update(_, _, _));
password_save_manager()->Save(observed_form_, parsed_submitted_form_);
}
TEST_F(MultiStorePasswordSaveManagerTest,
UpdateBothStoresIfCredentialsExistInProfileStoreOnly) {
TEST_F(MultiStorePasswordSaveManagerTest, UpdateInProfileStoreOnly) {
SetAccountStoreEnabled(/*is_enabled=*/true);
PasswordForm saved_match_in_profile_store(saved_match_);
......@@ -343,13 +341,12 @@ TEST_F(MultiStorePasswordSaveManagerTest,
EXPECT_TRUE(password_save_manager()->IsPasswordUpdate());
EXPECT_CALL(*mock_profile_form_saver(), Update(_, _, _));
EXPECT_CALL(*mock_account_form_saver(), Update(_, _, _));
EXPECT_CALL(*mock_account_form_saver(), Update(_, _, _)).Times(0);
password_save_manager()->Save(observed_form_, parsed_submitted_form_);
}
TEST_F(MultiStorePasswordSaveManagerTest,
UpdateBothStoresIfCredentialsExistInBothStores) {
TEST_F(MultiStorePasswordSaveManagerTest, UpdateInBothStores) {
SetAccountStoreEnabled(/*is_enabled=*/true);
PasswordForm saved_match_in_profile_store(saved_match_);
......
......@@ -157,7 +157,7 @@ void PasswordSaveManagerImpl::CreatePendingCredentials(
const FormData& submitted_form,
bool is_http_auth,
bool is_credential_api_save) {
const PasswordForm* similar_saved_form;
const PasswordForm* similar_saved_form = nullptr;
std::tie(similar_saved_form, pending_credentials_state_) =
FindSimilarSavedFormAndComputeState(parsed_submitted_form);
......@@ -451,29 +451,37 @@ void PasswordSaveManagerImpl::SavePendingToStore(
const PasswordForm& parsed_submitted_form) {
UploadVotesAndMetrics(observed_form, parsed_submitted_form);
bool update = !IsNewLogin();
const PasswordForm* similar_saved_form =
FindSimilarSavedFormAndComputeState(parsed_submitted_form).first;
if (update && !pending_credentials_.IsFederatedCredential())
DCHECK(similar_saved_form);
base::string16 old_password = similar_saved_form
? similar_saved_form->password_value
: base::string16();
if (HasGeneratedPassword()) {
generation_manager_->CommitGeneratedPassword(
pending_credentials_, form_fetcher_->GetAllRelevantMatches(),
old_password, GetFormSaverForGeneration());
} else if (update) {
GetOldPassword(parsed_submitted_form), GetFormSaverForGeneration());
} else {
SavePendingToStoreImpl(parsed_submitted_form);
}
}
void PasswordSaveManagerImpl::SavePendingToStoreImpl(
const PasswordForm& parsed_submitted_form) {
auto matches = form_fetcher_->GetAllRelevantMatches();
base::string16 old_password = GetOldPassword(parsed_submitted_form);
if (IsNewLogin()) {
form_saver_->Save(pending_credentials_, matches, old_password);
} else {
// It sounds wrong that we still update even if the state is NONE. We
// should double check if this actually necessary. Currently some tests
// depend on this behavior.
UpdateInternal(form_fetcher_->GetAllRelevantMatches(), old_password);
} else {
SaveInternal(form_fetcher_->GetAllRelevantMatches(), old_password);
form_saver_->Update(pending_credentials_, matches, old_password);
}
}
base::string16 PasswordSaveManagerImpl::GetOldPassword(
const PasswordForm& parsed_submitted_form) const {
const PasswordForm* similar_saved_form =
FindSimilarSavedFormAndComputeState(parsed_submitted_form).first;
return similar_saved_form ? similar_saved_form->password_value
: base::string16();
}
void PasswordSaveManagerImpl::UploadVotesAndMetrics(
const FormData& observed_form,
const PasswordForm& parsed_submitted_form) {
......@@ -530,18 +538,6 @@ PasswordSaveManagerImpl::GetRelevantMatchesForGeneration(
return matches;
}
void PasswordSaveManagerImpl::SaveInternal(
const std::vector<const PasswordForm*>& matches,
const base::string16& old_password) {
form_saver_->Save(pending_credentials_, matches, old_password);
}
void PasswordSaveManagerImpl::UpdateInternal(
const std::vector<const PasswordForm*>& matches,
const base::string16& old_password) {
form_saver_->Update(pending_credentials_, matches, old_password);
}
void PasswordSaveManagerImpl::CloneInto(PasswordSaveManagerImpl* clone) {
DCHECK(clone);
if (generation_manager_)
......
......@@ -111,13 +111,8 @@ class PasswordSaveManagerImpl : public PasswordSaveManager {
GetRelevantMatchesForGeneration(
const std::vector<const autofill::PasswordForm*>& matches);
virtual void SaveInternal(
const std::vector<const autofill::PasswordForm*>& matches,
const base::string16& old_password);
virtual void UpdateInternal(
const std::vector<const autofill::PasswordForm*>& matches,
const base::string16& old_password);
virtual void SavePendingToStoreImpl(
const autofill::PasswordForm& parsed_submitted_form);
// Clones the current object into |clone|. |clone| must not be null.
void CloneInto(PasswordSaveManagerImpl* clone);
......@@ -142,6 +137,9 @@ class PasswordSaveManagerImpl : public PasswordSaveManager {
const FormFetcher* form_fetcher_;
private:
base::string16 GetOldPassword(
const autofill::PasswordForm& parsed_submitted_form) const;
void SetVotesAndRecordMetricsForPendingCredentials(
const autofill::PasswordForm& parsed_submitted_form);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment