Settings: Introduce requestCredentialDetails and make note optional
This CL introduces requestCredentialDetails
which returns a PasswordUiEntry. The API is similar to
requestCredentialDetails. Since the requestCredentialDetails API is
still used (when the Password View subpage is disabled), I am keeping as
it is.
PasswordUiEntry now has the note field as optional, as the followup CLs
will put the note behind authentication.
Bug: 1345899
Change-Id: I94066bf4dcd5d474cdc4d51c880fb1d32710cbd6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3784370
Reviewed-by: Demetrios Papadopoulos <dpapad@chromium.org>
Reviewed-by: Istiaque Ahmed <lazyboy@chromium.org>
Reviewed-by: Viktor Semeniuk <vsemeniuk@google.com>
Commit-Queue: Adem Derinel <derinel@google.com>
Cr-Commit-Position: refs/heads/main@{#1032467}
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_api.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_api.cc
index 9f7d8aa..6bd15382 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_api.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_api.cc
@@ -124,6 +124,40 @@
->id)));
}
+// PasswordsPrivateRequestCredentialDetailsFunction
+ResponseAction PasswordsPrivateRequestCredentialDetailsFunction::Run() {
+ auto parameters =
+ api::passwords_private::RequestCredentialDetails::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(parameters);
+
+ GetDelegate(browser_context())
+ ->RequestCredentialDetails(
+ parameters->id,
+ base::BindOnce(&PasswordsPrivateRequestCredentialDetailsFunction::
+ GotPasswordUiEntry,
+ this),
+ GetSenderWebContents());
+
+ // GotPasswordUiEntry() might have responded before we reach this point.
+ return did_respond() ? AlreadyResponded() : RespondLater();
+}
+
+void PasswordsPrivateRequestCredentialDetailsFunction::GotPasswordUiEntry(
+ absl::optional<api::passwords_private::PasswordUiEntry> password_ui_entry) {
+ if (password_ui_entry) {
+ Respond(ArgumentList(
+ api::passwords_private::RequestCredentialDetails::Results::Create(
+ std::move(*password_ui_entry))));
+ return;
+ }
+
+ Respond(Error(base::StringPrintf(
+ "Could not obtain password entry. Either the user is not "
+ "authenticated or no credential with id = %d could be found.",
+ api::passwords_private::RequestCredentialDetails::Params::Create(args())
+ ->id)));
+}
+
// PasswordsPrivateGetSavedPasswordListFunction
ResponseAction PasswordsPrivateGetSavedPasswordListFunction::Run() {
// GetList() can immediately call GotList() (which would Respond() before
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_api.h b/chrome/browser/extensions/api/passwords_private/passwords_private_api.h
index 64e668cf..7a4b1bb 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_api.h
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_api.h
@@ -97,6 +97,23 @@
void GotPassword(absl::optional<std::u16string> password);
};
+class PasswordsPrivateRequestCredentialDetailsFunction
+ : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("passwordsPrivate.requestCredentialDetails",
+ PASSWORDSPRIVATE_REQUESTCREDENTIALDETAILS)
+ protected:
+ ~PasswordsPrivateRequestCredentialDetailsFunction() override = default;
+
+ // ExtensionFunction overrides.
+ ResponseAction Run() override;
+
+ private:
+ void GotPasswordUiEntry(
+ absl::optional<api::passwords_private::PasswordUiEntry>
+ password_ui_entry);
+};
+
class PasswordsPrivateGetSavedPasswordListFunction : public ExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION("passwordsPrivate.getSavedPasswordList",
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_apitest.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_apitest.cc
index cece829..f917abe 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_apitest.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_apitest.cc
@@ -200,6 +200,15 @@
EXPECT_TRUE(RunPasswordsSubtest("requestPlaintextPasswordFails")) << message_;
}
+IN_PROC_BROWSER_TEST_F(PasswordsPrivateApiTest, RequestCredentialDetails) {
+ EXPECT_TRUE(RunPasswordsSubtest("requestCredentialDetails")) << message_;
+}
+
+IN_PROC_BROWSER_TEST_F(PasswordsPrivateApiTest, RequestCredentialDetailsFails) {
+ ResetPlaintextPassword();
+ EXPECT_TRUE(RunPasswordsSubtest("requestCredentialDetailsFails")) << message_;
+}
+
IN_PROC_BROWSER_TEST_F(PasswordsPrivateApiTest, GetSavedPasswordList) {
EXPECT_TRUE(RunPasswordsSubtest("getSavedPasswordList")) << message_;
}
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h
index 3079618e..ac1301fa 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate.h
@@ -33,6 +33,8 @@
public:
using PlaintextPasswordCallback =
base::OnceCallback<void(absl::optional<std::u16string>)>;
+ using RequestCredentialDetailsCallback = base::OnceCallback<void(
+ absl::optional<api::passwords_private::PasswordUiEntry>)>;
using RefreshScriptsIfNecessaryCallback = base::OnceClosure;
@@ -118,6 +120,20 @@
PlaintextPasswordCallback callback,
content::WebContents* web_contents) = 0;
+ // Requests the full PasswordUiEntry (with filled password) with the given id.
+ // Returns the full PasswordUiEntry with |callback|. Returns |absl::nullopt|
+ // if no matching credential with |id| is found.
+ // |id| the id created when going over the list of saved passwords.
+ // |reason| The reason why the full PasswordUiEntry is requested.
+ // |callback| The callback that gets invoked with the PasswordUiEntry if it
+ // could be obtained successfully, or absl::nullopt otherwise.
+ // |web_contents| The web content object used as the UI; will be used to show
+ // an OS-level authentication dialog if necessary.
+ virtual void RequestCredentialDetails(
+ int id,
+ RequestCredentialDetailsCallback callback,
+ content::WebContents* web_contents) = 0;
+
// Moves a list of passwords currently stored on the device to being stored in
// the signed-in, non-syncing Google Account. The result of any password is a
// no-op if any of these is true: |id| is invalid; |id| corresponds to a
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.cc
index 4e123a4..cce390f 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.cc
@@ -149,6 +149,34 @@
return {};
}
+extensions::api::passwords_private::PasswordUiEntry
+CreatePasswordUiEntryFromCredentialUiEntry(
+ int id,
+ const CredentialUIEntry& credential) {
+ extensions::api::passwords_private::PasswordUiEntry entry;
+ entry.urls = extensions::CreateUrlCollectionFromCredential(credential);
+ entry.username = base::UTF16ToUTF8(credential.username);
+ // TODO(crbug.com/1345899): Fill the note field after authentication in
+ // OnRequestCredentialDetailsAuthResult
+ entry.note =
+ std::make_unique<std::string>(base::UTF16ToUTF8(credential.note.value));
+ entry.id = id;
+ entry.stored_in = extensions::StoreSetFromCredential(credential);
+ entry.is_android_credential =
+ password_manager::IsValidAndroidFacetURI(credential.signon_realm);
+ if (!credential.federation_origin.opaque()) {
+ std::u16string formatted_origin =
+ url_formatter::FormatOriginForSecurityDisplay(
+ credential.federation_origin,
+ url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC);
+
+ entry.federation_text =
+ std::make_unique<std::string>(l10n_util::GetStringFUTF8(
+ IDS_PASSWORDS_VIA_FEDERATION, formatted_origin));
+ }
+ return entry;
+}
+
} // namespace
namespace extensions {
@@ -354,6 +382,23 @@
weak_ptr_factory_.GetWeakPtr(), id, reason, std::move(callback)));
}
+void PasswordsPrivateDelegateImpl::RequestCredentialDetails(
+ int id,
+ RequestCredentialDetailsCallback callback,
+ content::WebContents* web_contents) {
+ // Save |web_contents| so that it can be used later when OsReauthCall() is
+ // called. Note: This is safe because the |web_contents| is used before
+ // exiting this method.
+ // TODO(crbug.com/495290): Pass the native window directly to the
+ // reauth-handling code.
+ web_contents_ = web_contents;
+ password_access_authenticator_.EnsureUserIsAuthenticated(
+ GetReauthPurpose(api::passwords_private::PLAINTEXT_REASON_VIEW),
+ base::BindOnce(
+ &PasswordsPrivateDelegateImpl::OnRequestCredentialDetailsAuthResult,
+ weak_ptr_factory_.GetWeakPtr(), id, std::move(callback)));
+}
+
void PasswordsPrivateDelegateImpl::OsReauthCall(
password_manager::ReauthPurpose purpose,
password_manager::PasswordAccessAuthenticator::AuthResultCallback
@@ -393,26 +438,8 @@
current_exception_entry.id = id;
current_exceptions_.push_back(std::move(current_exception_entry));
} else {
- api::passwords_private::PasswordUiEntry entry;
- entry.urls = CreateUrlCollectionFromCredential(credential);
- entry.username = base::UTF16ToUTF8(credential.username);
- entry.note = base::UTF16ToUTF8(credential.note.value);
- entry.id = id;
- entry.stored_in = StoreSetFromCredential(credential);
- entry.is_android_credential =
- password_manager::IsValidAndroidFacetURI(credential.signon_realm);
- if (!credential.federation_origin.opaque()) {
- std::u16string formatted_origin =
- url_formatter::FormatOriginForSecurityDisplay(
- credential.federation_origin,
- url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC);
-
- entry.federation_text =
- std::make_unique<std::string>(l10n_util::GetStringFUTF8(
- IDS_PASSWORDS_VIA_FEDERATION, formatted_origin));
- }
-
- current_entries_.push_back(std::move(entry));
+ current_entries_.push_back(
+ CreatePasswordUiEntryFromCredentialUiEntry(id, credential));
}
}
@@ -648,22 +675,32 @@
} else {
std::move(callback).Run(entry->password);
}
+ EmitHistogramsForCredentialAccess(*entry, reason);
+}
- syncer::SyncService* sync_service = nullptr;
- if (SyncServiceFactory::HasSyncService(profile_)) {
- sync_service = SyncServiceFactory::GetForProfile(profile_);
- }
- if (password_manager::sync_util::IsSyncAccountCredential(
- entry->url, entry->username, sync_service,
- IdentityManagerFactory::GetForProfile(profile_))) {
- base::RecordAction(
- base::UserMetricsAction("PasswordManager_SyncCredentialShown"));
+void PasswordsPrivateDelegateImpl::OnRequestCredentialDetailsAuthResult(
+ int id,
+ RequestCredentialDetailsCallback callback,
+ bool authenticated) {
+ if (!authenticated) {
+ std::move(callback).Run(absl::nullopt);
+ return;
}
- UMA_HISTOGRAM_ENUMERATION(
- "PasswordManager.AccessPasswordInSettings",
- ConvertPlaintextReason(reason),
- password_manager::metrics_util::ACCESS_PASSWORD_COUNT);
+ const CredentialUIEntry* credential = credential_id_generator_.TryGetKey(id);
+ if (!credential) {
+ std::move(callback).Run(absl::nullopt);
+ return;
+ }
+
+ api::passwords_private::PasswordUiEntry password_ui_entry =
+ CreatePasswordUiEntryFromCredentialUiEntry(id, *credential);
+ password_ui_entry.password =
+ std::make_unique<std::string>(base::UTF16ToUTF8(credential->password));
+ std::move(callback).Run(std::move(password_ui_entry));
+
+ EmitHistogramsForCredentialAccess(
+ *credential, api::passwords_private::PLAINTEXT_REASON_VIEW);
}
void PasswordsPrivateDelegateImpl::OnExportPasswordsAuthResult(
@@ -718,4 +755,24 @@
pre_initialization_callbacks_.clear();
}
+void PasswordsPrivateDelegateImpl::EmitHistogramsForCredentialAccess(
+ const CredentialUIEntry& entry,
+ api::passwords_private::PlaintextReason reason) {
+ syncer::SyncService* sync_service = nullptr;
+ if (SyncServiceFactory::HasSyncService(profile_)) {
+ sync_service = SyncServiceFactory::GetForProfile(profile_);
+ }
+ if (password_manager::sync_util::IsSyncAccountCredential(
+ entry.url, entry.username, sync_service,
+ IdentityManagerFactory::GetForProfile(profile_))) {
+ base::RecordAction(
+ base::UserMetricsAction("PasswordManager_SyncCredentialShown"));
+ }
+
+ UMA_HISTOGRAM_ENUMERATION(
+ "PasswordManager.AccessPasswordInSettings",
+ ConvertPlaintextReason(reason),
+ password_manager::metrics_util::ACCESS_PASSWORD_COUNT);
+}
+
} // namespace extensions
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.h b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.h
index fe616f8..37c48e7d 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.h
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl.h
@@ -25,6 +25,7 @@
#include "components/password_manager/core/browser/password_access_authenticator.h"
#include "components/password_manager/core/browser/password_account_storage_settings_watcher.h"
#include "components/password_manager/core/browser/reauth_purpose.h"
+#include "components/password_manager/core/browser/ui/credential_ui_entry.h"
#include "components/password_manager/core/browser/ui/export_progress_status.h"
#include "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
#include "extensions/browser/extension_function.h"
@@ -75,6 +76,9 @@
api::passwords_private::PlaintextReason reason,
PlaintextPasswordCallback callback,
content::WebContents* web_contents) override;
+ void RequestCredentialDetails(int id,
+ RequestCredentialDetailsCallback callback,
+ content::WebContents* web_contents) override;
void MovePasswordsToAccount(const std::vector<int>& ids,
content::WebContents* web_contents) override;
void ImportPasswords(content::WebContents* web_contents) override;
@@ -162,6 +166,12 @@
PlaintextPasswordCallback callback,
bool authenticated);
+ // Callback for RequestCredentialDetails() after authentication check.
+ void OnRequestCredentialDetailsAuthResult(
+ int id,
+ RequestCredentialDetailsCallback callback,
+ bool authenticated);
+
// Callback for ExportPasswords() after authentication check.
void OnExportPasswordsAuthResult(
base::OnceCallback<void(const std::string&)> accepted_callback,
@@ -179,6 +189,11 @@
password_manager::PasswordAccessAuthenticator::AuthResultCallback
callback);
+ // Records user action and emits histogram values for retrieving |entry|.
+ void EmitHistogramsForCredentialAccess(
+ const password_manager::CredentialUIEntry& entry,
+ api::passwords_private::PlaintextReason reason);
+
// Not owned by this class.
raw_ptr<Profile> profile_;
diff --git a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc
index 5511fc4b..a9dc3d4 100644
--- a/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc
+++ b/chrome/browser/extensions/api/passwords_private/passwords_private_delegate_impl_unittest.cc
@@ -68,6 +68,8 @@
using MockPlaintextPasswordCallback =
base::MockCallback<PasswordsPrivateDelegate::PlaintextPasswordCallback>;
+using MockRequestCredentialDetailsCallback = base::MockCallback<
+ PasswordsPrivateDelegate::RequestCredentialDetailsCallback>;
class MockPasswordManagerClient : public ChromePasswordManagerClient {
public:
@@ -197,7 +199,7 @@
MATCHER_P(PasswordUiEntryDataEquals, expected, "") {
return testing::Value(expected.get().urls.link, arg.urls.link) &&
testing::Value(expected.get().username, arg.username) &&
- testing::Value(expected.get().note, arg.note) &&
+ testing::Value(*expected.get().note, *arg.note) &&
testing::Value(expected.get().stored_in, arg.stored_in) &&
testing::Value(expected.get().is_android_credential,
arg.is_android_credential);
@@ -381,13 +383,13 @@
api::passwords_private::PasswordUiEntry expected_entry1;
expected_entry1.urls.link = "https://example1.com/";
expected_entry1.username = "username1";
- expected_entry1.note = "";
+ expected_entry1.note = std::make_unique<std::string>();
expected_entry1.stored_in =
api::passwords_private::PASSWORD_STORE_SET_ACCOUNT;
api::passwords_private::PasswordUiEntry expected_entry2;
expected_entry2.urls.link = "http://example2.com/login";
expected_entry2.username = "";
- expected_entry2.note = "note";
+ expected_entry2.note = std::make_unique<std::string>("note");
expected_entry2.stored_in = api::passwords_private::PASSWORD_STORE_SET_DEVICE;
EXPECT_CALL(callback,
Run(testing::UnorderedElementsAre(
@@ -473,7 +475,7 @@
EXPECT_CALL(callback, Run(SizeIs(1)))
.WillOnce([](const PasswordsPrivateDelegate::UiEntries& passwords) {
EXPECT_EQ("new_user", passwords[0].username);
- EXPECT_EQ("", passwords[0].note);
+ EXPECT_THAT(passwords[0].note, Pointee(Eq("")));
});
delegate.GetSavedPasswordsList(callback.Get());
}
@@ -499,8 +501,8 @@
.WillOnce([&](const PasswordsPrivateDelegate::UiEntries& passwords) {
EXPECT_EQ(sample_form.username_value,
base::UTF8ToUTF16(passwords[0].username));
- EXPECT_EQ(sample_form.notes[1].value,
- base::UTF8ToUTF16(passwords[0].note));
+ EXPECT_THAT(passwords[0].note,
+ Pointee(Eq(base::UTF16ToUTF8(sample_form.notes[1].value))));
});
delegate.GetSavedPasswordsList(callback.Get());
int sample_form_id = delegate.GetIdForCredential(
@@ -527,7 +529,7 @@
EXPECT_CALL(callback, Run(SizeIs(1)))
.WillOnce([](const PasswordsPrivateDelegate::UiEntries& passwords) {
EXPECT_EQ("new_user", passwords[0].username);
- EXPECT_EQ("new note", passwords[0].note);
+ EXPECT_THAT(passwords[0].note, Pointee(Eq("new note")));
});
delegate.GetSavedPasswordsList(callback.Get());
}
@@ -694,7 +696,7 @@
delegate.RequestPlaintextPassword(
0, api::passwords_private::PLAINTEXT_REASON_COPY, password_callback.Get(),
nullptr);
- // Clipboard should not be modifiend in case Reauth failed
+ // Clipboard should not be modified in case Reauth failed
std::u16string result;
test_clipboard_->ReadText(ui::ClipboardBuffer::kCopyPaste,
/* data_dst = */ nullptr, &result);
@@ -732,6 +734,38 @@
1);
}
+TEST_F(PasswordsPrivateDelegateImplTest,
+ TestPassedReauthOnRequestCredentialDetails) {
+ SetUpPasswordStores({CreateSampleForm()});
+
+ PasswordsPrivateDelegateImpl delegate(&profile_);
+ // Spin the loop to allow PasswordStore tasks posted on the creation of
+ // |delegate| to be completed.
+ base::RunLoop().RunUntilIdle();
+
+ MockReauthCallback callback;
+ delegate.set_os_reauth_call(callback.Get());
+
+ EXPECT_CALL(callback, Run(ReauthPurpose::VIEW_PASSWORD, _))
+ .WillOnce(testing::WithArg<1>(
+ [&](password_manager::PasswordAccessAuthenticator::AuthResultCallback
+ callback) { std::move(callback).Run(true); }));
+
+ MockRequestCredentialDetailsCallback password_callback;
+ EXPECT_CALL(password_callback, Run)
+ .WillOnce(
+ [&](absl::optional<api::passwords_private::PasswordUiEntry> entry) {
+ EXPECT_THAT(entry->password, Pointee(Eq("test")));
+ EXPECT_THAT(entry->username, Eq("test@gmail.com"));
+ });
+
+ delegate.RequestCredentialDetails(0, password_callback.Get(), nullptr);
+
+ histogram_tester().ExpectUniqueSample(
+ kHistogramName, password_manager::metrics_util::ACCESS_PASSWORD_VIEWED,
+ 1);
+}
+
TEST_F(PasswordsPrivateDelegateImplTest, TestFailedReauthOnView) {
SetUpPasswordStores({CreateSampleForm()});
@@ -758,6 +792,31 @@
histogram_tester().ExpectTotalCount(kHistogramName, 0);
}
+TEST_F(PasswordsPrivateDelegateImplTest,
+ TestFailedReauthOnRequestCredentialDetails) {
+ SetUpPasswordStores({CreateSampleForm()});
+
+ PasswordsPrivateDelegateImpl delegate(&profile_);
+ // Spin the loop to allow PasswordStore tasks posted on the creation of
+ // |delegate| to be completed.
+ base::RunLoop().RunUntilIdle();
+
+ MockReauthCallback callback;
+ delegate.set_os_reauth_call(callback.Get());
+
+ EXPECT_CALL(callback, Run(ReauthPurpose::VIEW_PASSWORD, _))
+ .WillOnce(testing::WithArg<1>(
+ [&](password_manager::PasswordAccessAuthenticator::AuthResultCallback
+ callback) { std::move(callback).Run(false); }));
+
+ MockRequestCredentialDetailsCallback password_callback;
+ EXPECT_CALL(password_callback, Run(Eq(absl::nullopt)));
+ delegate.RequestCredentialDetails(0, password_callback.Get(), nullptr);
+
+ // Since Reauth had failed password was not viewed and metric wasn't recorded
+ histogram_tester().ExpectTotalCount(kHistogramName, 0);
+}
+
TEST_F(PasswordsPrivateDelegateImplTest, TestReauthFailedOnExport) {
SetUpPasswordStores({CreateSampleForm()});
StrictMock<base::MockCallback<base::OnceCallback<void(const std::string&)>>>
@@ -899,7 +958,7 @@
expected_entry.urls.link =
"https://play.google.com/store/apps/details?id=example.com";
expected_entry.username = "test@gmail.com";
- expected_entry.note = "";
+ expected_entry.note = std::make_unique<std::string>();
expected_entry.is_android_credential = true;
expected_entry.stored_in = api::passwords_private::PASSWORD_STORE_SET_DEVICE;
EXPECT_CALL(callback, Run(testing::ElementsAre(PasswordUiEntryDataEquals(
diff --git a/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.cc b/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.cc
index c883996..d0dcbd7 100644
--- a/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.cc
+++ b/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.cc
@@ -151,6 +151,20 @@
std::move(callback).Run(plaintext_password_);
}
+void TestPasswordsPrivateDelegate::RequestCredentialDetails(
+ int id,
+ RequestCredentialDetailsCallback callback,
+ content::WebContents* web_contents) {
+ api::passwords_private::PasswordUiEntry entry = CreateEntry(42);
+ if (plaintext_password_.has_value()) {
+ entry.password = std::make_unique<std::string>(
+ base::UTF16ToUTF8(plaintext_password_.value()));
+ std::move(callback).Run(std::move(entry));
+ } else {
+ std::move(callback).Run(std::move(absl::nullopt));
+ }
+}
+
void TestPasswordsPrivateDelegate::MovePasswordsToAccount(
const std::vector<int>& ids,
content::WebContents* web_contents) {
diff --git a/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h b/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h
index 7894b9a8..6ace7622 100644
--- a/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h
+++ b/chrome/browser/extensions/api/passwords_private/test_passwords_private_delegate.h
@@ -54,6 +54,9 @@
api::passwords_private::PlaintextReason reason,
PlaintextPasswordCallback callback,
content::WebContents* web_contents) override;
+ void RequestCredentialDetails(int id,
+ RequestCredentialDetailsCallback callback,
+ content::WebContents* web_contents) override;
void MovePasswordsToAccount(const std::vector<int>& ids,
content::WebContents* web_contents) override;
void ImportPasswords(content::WebContents* web_contents) override;
diff --git a/chrome/browser/resources/settings/autofill_page/password_edit_dialog.ts b/chrome/browser/resources/settings/autofill_page/password_edit_dialog.ts
index eaa303dc..5386ca7 100644
--- a/chrome/browser/resources/settings/autofill_page/password_edit_dialog.ts
+++ b/chrome/browser/resources/settings/autofill_page/password_edit_dialog.ts
@@ -299,7 +299,7 @@
if (this.existingEntry) {
this.websiteUrls_ = this.existingEntry.urls;
this.username_ = this.existingEntry.username;
- this.note_ = this.existingEntry.note;
+ this.note_ = this.existingEntry.note || '';
}
this.password_ = this.getPassword_();
if (!this.isInFederatedViewMode_) {
diff --git a/chrome/common/extensions/api/passwords_private.idl b/chrome/common/extensions/api/passwords_private.idl
index 0143ac5..d737948 100644
--- a/chrome/common/extensions/api/passwords_private.idl
+++ b/chrome/common/extensions/api/passwords_private.idl
@@ -170,7 +170,7 @@
DOMString username;
// The password of the credential. Empty by default, only set if explicitly
- // requested. Populated exclusively by JS.
+ // requested.
DOMString? password;
// Text shown if the password was obtained via a federated identity.
@@ -186,7 +186,7 @@
boolean isAndroidCredential;
// The value of the attached note.
- DOMString note;
+ DOMString? note;
// The URL where the insecure password can be changed. Might be not set for
// Android apps.
@@ -268,6 +268,7 @@
};
callback PlaintextPasswordCallback = void(DOMString password);
+ callback RequestCredentialDetailsCallback = void(PasswordUiEntry entry);
callback ChangeSavedPasswordCallback = void(long newId);
callback PasswordListCallback = void(PasswordUiEntry[] entries);
callback ExceptionListCallback = void(ExceptionEntry[] exceptions);
@@ -323,6 +324,17 @@
PlaintextReason reason,
PlaintextPasswordCallback callback);
+ // Returns the PasswordUiEntry (with |password| field filled) corresponding
+ // to |id|. Note that on some operating systems, this call may result in an
+ // OS-level reauthentication. Once the PasswordUiEntry has been fetched, it
+ // will be returned via |callback|.
+ // |id|: The id for the password entry being being retrieved.
+ // |callback|: The callback that gets invoked with the retrieved
+ // PasswordUiEntry.
+ [supportPromises] static void requestCredentialDetails(
+ long id,
+ RequestCredentialDetailsCallback callback);
+
// Returns the list of saved passwords.
// |callback|: Called with the list of saved passwords.
[supportsPromises] static void getSavedPasswordList(
diff --git a/chrome/test/data/extensions/api_test/passwords_private/test.js b/chrome/test/data/extensions/api_test/passwords_private/test.js
index e4ab2d7..094ebdb4 100644
--- a/chrome/test/data/extensions/api_test/passwords_private/test.js
+++ b/chrome/test/data/extensions/api_test/passwords_private/test.js
@@ -197,6 +197,27 @@
});
},
+ function requestCredentialDetails() {
+ chrome.passwordsPrivate.requestCredentialDetails(0, passwordUiEntry => {
+ // Ensure that the callback is invoked without an error state and the
+ // expected plaintext password.
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq('plaintext', passwordUiEntry.password);
+ chrome.test.succeed();
+ });
+ },
+
+ function requestCredentialDetailsFails() {
+ chrome.passwordsPrivate.requestCredentialDetails(123, passwordUiEntry => {
+ // Ensure that the callback is invoked with an error state and the
+ // message contains the right id.
+ chrome.test.assertLastError(
+ 'Could not obtain password entry. Either the user is not ' +
+ 'authenticated or no credential with id = 123 could be found.');
+ chrome.test.succeed();
+ });
+ },
+
function getSavedPasswordList() {
var callback = function(list) {
chrome.test.assertTrue(!!list);
diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h
index 5edf1d4d..e95fcd4 100644
--- a/extensions/browser/extension_function_histogram_value.h
+++ b/extensions/browser/extension_function_histogram_value.h
@@ -1757,6 +1757,7 @@
FILEMANAGERPRIVATE_SHOWDLPRESTRICTIONDETAILS = 1694,
ACCESSIBILITY_PRIVATE_SILENCESPOKENFEEDBACK = 1695,
AUTOTESTPRIVATE_SETALLOWEDPREF = 1696,
+ PASSWORDSPRIVATE_REQUESTCREDENTIALDETAILS = 1697,
// Last entry: Add new entries above, then run:
// tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY
diff --git a/third_party/closure_compiler/externs/passwords_private.js b/third_party/closure_compiler/externs/passwords_private.js
index b96870f..37b0a2aa 100644
--- a/third_party/closure_compiler/externs/passwords_private.js
+++ b/third_party/closure_compiler/externs/passwords_private.js
@@ -143,7 +143,7 @@
* id: number,
* storedIn: !chrome.passwordsPrivate.PasswordStoreSet,
* isAndroidCredential: boolean,
- * note: string,
+ * note: (string|undefined),
* changePasswordUrl: (string|undefined),
* hasStartableScript: boolean,
* compromisedInfo: (!chrome.passwordsPrivate.CompromisedInfo|undefined)
@@ -249,6 +249,17 @@
chrome.passwordsPrivate.requestPlaintextPassword = function(id, reason, callback) {};
/**
+ * Returns the PasswordUiEntry (with |password| field filled) corresponding to
+ * |id|. Note that on some operating systems, this call may result in an
+ * OS-level reauthentication. Once the PasswordUiEntry has been fetched, it will
+ * be returned via |callback|.
+ * @param {number} id The id for the password entry being being retrieved.
+ * @param {function(!chrome.passwordsPrivate.PasswordUiEntry): void} callback
+ * The callback that gets invoked with the retrieved PasswordUiEntry.
+ */
+chrome.passwordsPrivate.requestCredentialDetails = function(id, callback) {};
+
+/**
* Returns the list of saved passwords.
* @param {function(!Array<!chrome.passwordsPrivate.PasswordUiEntry>): void}
* callback Called with the list of saved passwords.
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 7be88e6d..a709061 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -35241,6 +35241,7 @@
<int value="1694" label="FILEMANAGERPRIVATE_SHOWDLPRESTRICTIONDETAILS"/>
<int value="1695" label="ACCESSIBILITY_PRIVATE_SILENCESPOKENFEEDBACK"/>
<int value="1696" label="AUTOTESTPRIVATE_SETALLOWEDPREF"/>
+ <int value="1697" label="PASSWORDSPRIVATE_REQUESTCREDENTIALDETAILS"/>
</enum>
<enum name="ExtensionIconState">
diff --git a/tools/typescript/definitions/passwords_private.d.ts b/tools/typescript/definitions/passwords_private.d.ts
index ca79875..199b183 100644
--- a/tools/typescript/definitions/passwords_private.d.ts
+++ b/tools/typescript/definitions/passwords_private.d.ts
@@ -105,7 +105,7 @@
id: number;
storedIn: PasswordStoreSet;
isAndroidCredential: boolean;
- note: string;
+ note?: string;
changePasswordUrl?: string;
hasStartableScript: boolean;
compromisedInfo?: CompromisedInfo;
@@ -146,12 +146,16 @@
export function changeSavedPassword(
id: number, params: ChangeSavedPasswordParams,
callback?: (newId: number) => void): void;
- export function removeSavedPassword(id: number, fromStores: PasswordStoreSet): void;
+ export function removeSavedPassword(
+ id: number, fromStores: PasswordStoreSet): void;
export function removePasswordException(id: number): void;
export function undoRemoveSavedPasswordOrException(): void;
export function requestPlaintextPassword(
id: number, reason: PlaintextReason,
callback: (password: string) => void): void;
+ export function requestCredentialDetails(
+ id: number,
+ callback: (passwordUiEntry: PasswordUiEntry) => void): void;
export function getSavedPasswordList(
callback: (entries: Array<PasswordUiEntry>) => void): void;
export function getPasswordExceptionList(