Commit f2a7d302 authored by Jordy Greenblatt's avatar Jordy Greenblatt Committed by Commit Bot

[CrOS Settings] Require GAIA password to view saved passwords in CrOS

The goal of this CL is to protect passwords saved to the Chrome browser
running on CrOS by requiring a GAIA password entry within the last
minute to show a saved password in settings and a fresh auth token [1]
to export passwords to a .csv.

The key steps for changing the password flow for CrOS were
 -Provide passwords-section with a authToken and bound to a
  password-prompt-dialog
 -On "show password" clicks, check if there is a valid there is a valid
  and <1 minute old auth token [2]
 -If not, prompt the user for their password and check again
 -If so, go back to the old flow.

The new steps in the export passwords flow are almost the same except
that the password prompt comes up on every request [3].

The less intuitive parts of the code come from trying to tinker as little
as possible with security an UX for non CrOS users. In particular, I added
and infrastructure for pausing the password flows to allow the user to
enter their password and then resume the flow when the auth token is
received [4] and incorporated an auth token check in a way that fits the
general OS specific password safety flow [5].

I also added CrOS specific browser tests in a new file and refactored the
existing password-section browser tests to share utilities with the new
file. Beyond that there were some minor changes to the existing file
that the change required due to the more involved Polymer event
dependencies for CrOS.

---

Testing:

Beyond the browser tests, I manually tested on CrOS by
 -going through a flow of entering an incorrect password, cancelling, and
  then entering the correct password for all the affected elements
 -checking that I could freely show passwords for a minute after a
  successful password entry/auth token request
 -checking that I could not export passwords or get plaintext passwords
  after the respective deadlines of 5 seconds and 1 second through the
  standard user flow or by manually entering the requests directly into
  the JS console.

I also tested on Linux (i.e. my workstation) and Mac (a corp loaner) to
ensure that the functionality was not changed.

---

[1] In practice we require it be within the last 5 seconds because it saved
some work and provided comparable security.
[2] The auth token does not need to be passed through the
passwordsPrivate API because there is a single auth token for the
machine so just checking its age ensures that the user entered their
password in the last minute and provided it to the quickUnlockPrivate
API.
[3] Since the 5 second expiration is a proxy for immediate expiration and
in practice will rarely allow for reusing a token, we can prompt the
password on each attempt for a more consistent UX.
[4] See AuthTokenRequestorBehavior and the 'request-auth-token-refresh'
event.
[5] See PasswordsPrivateDelegateImpl::OsReauthCall().


Bug: 917178
Change-Id: I8d8e988270ff627668f2022c093e558fa0409a63
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1574684
Commit-Queue: Jordy Greenblatt <jordynass@chromium.org>
Reviewed-by: default avatarHector Carmona <hcarmona@chromium.org>
Reviewed-by: default avatarSteven Bennetts <stevenjb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#661094}
parent 0df9cb07
...@@ -299,6 +299,7 @@ void QuickUnlockPrivateGetAuthTokenFunction::OnAuthSuccess( ...@@ -299,6 +299,7 @@ void QuickUnlockPrivateGetAuthTokenFunction::OnAuthSuccess(
Profile* profile = GetActiveProfile(browser_context()); Profile* profile = GetActiveProfile(browser_context());
QuickUnlockStorage* quick_unlock_storage = QuickUnlockStorage* quick_unlock_storage =
chromeos::quick_unlock::QuickUnlockFactory::GetForProfile(profile); chromeos::quick_unlock::QuickUnlockFactory::GetForProfile(profile);
quick_unlock_storage->MarkStrongAuth();
result->token = quick_unlock_storage->CreateAuthToken(user_context); result->token = quick_unlock_storage->CreateAuthToken(user_context);
result->lifetime_seconds = AuthToken::kTokenExpirationSeconds; result->lifetime_seconds = AuthToken::kTokenExpirationSeconds;
......
...@@ -31,6 +31,10 @@ ...@@ -31,6 +31,10 @@
#include "chrome/browser/password_manager/password_manager_util_win.h" #include "chrome/browser/password_manager/password_manager_util_win.h"
#elif defined(OS_MACOSX) #elif defined(OS_MACOSX)
#include "chrome/browser/password_manager/password_manager_util_mac.h" #include "chrome/browser/password_manager/password_manager_util_mac.h"
#elif defined(OS_CHROMEOS)
#include "chrome/browser/chromeos/login/quick_unlock/auth_token.h"
#include "chrome/browser/chromeos/login/quick_unlock/quick_unlock_factory.h"
#include "chrome/browser/chromeos/login/quick_unlock/quick_unlock_storage.h"
#endif #endif
namespace { namespace {
...@@ -41,6 +45,14 @@ const char kExportInProgress[] = "in-progress"; ...@@ -41,6 +45,14 @@ const char kExportInProgress[] = "in-progress";
// The error message returned to the UI when the user fails to reauthenticate. // The error message returned to the UI when the user fails to reauthenticate.
const char kReauthenticationFailed[] = "reauth-failed"; const char kReauthenticationFailed[] = "reauth-failed";
#if defined(OS_CHROMEOS)
constexpr static base::TimeDelta kShowPasswordAuthTokenLifetime =
base::TimeDelta::FromSeconds(
PasswordAccessAuthenticator::kAuthValidityPeriodSeconds);
constexpr static base::TimeDelta kExportPasswordsAuthTokenLifetime =
base::TimeDelta::FromSeconds(5);
#endif
// Map password_manager::ExportProgressStatus to // Map password_manager::ExportProgressStatus to
// extensions::api::passwords_private::ExportProgressStatus. // extensions::api::passwords_private::ExportProgressStatus.
extensions::api::passwords_private::ExportProgressStatus ConvertStatus( extensions::api::passwords_private::ExportProgressStatus ConvertStatus(
...@@ -204,6 +216,18 @@ bool PasswordsPrivateDelegateImpl::OsReauthCall( ...@@ -204,6 +216,18 @@ bool PasswordsPrivateDelegateImpl::OsReauthCall(
web_contents_->GetTopLevelNativeWindow(), purpose); web_contents_->GetTopLevelNativeWindow(), purpose);
#elif defined(OS_MACOSX) #elif defined(OS_MACOSX)
return password_manager_util_mac::AuthenticateUser(purpose); return password_manager_util_mac::AuthenticateUser(purpose);
#elif defined(OS_CHROMEOS)
chromeos::quick_unlock::QuickUnlockStorage* quick_unlock_storage =
chromeos::quick_unlock::QuickUnlockFactory::GetForProfile(profile_);
const chromeos::quick_unlock::AuthToken* auth_token =
quick_unlock_storage->GetAuthToken();
if (!auth_token || !auth_token->GetAge())
return false;
const base::TimeDelta auth_token_lifespan =
(purpose == password_manager::ReauthPurpose::EXPORT)
? kExportPasswordsAuthTokenLifetime
: kShowPasswordAuthTokenLifetime;
return auth_token->GetAge() <= auth_token_lifespan;
#else #else
return true; return true;
#endif #endif
......
...@@ -9,6 +9,7 @@ js_type_check("closure_compile") { ...@@ -9,6 +9,7 @@ js_type_check("closure_compile") {
":address_edit_dialog", ":address_edit_dialog",
":autofill_page", ":autofill_page",
":autofill_section", ":autofill_section",
":blocking_request_manager",
":credit_card_edit_dialog", ":credit_card_edit_dialog",
":credit_card_list", ":credit_card_list",
":credit_card_list_entry", ":credit_card_list_entry",
...@@ -50,6 +51,9 @@ js_library("autofill_section") { ...@@ -50,6 +51,9 @@ js_library("autofill_section") {
externs_list = [ "$externs_path/autofill_private.js" ] externs_list = [ "$externs_path/autofill_private.js" ]
} }
js_library("blocking_request_manager") {
}
js_library("payments_section") { js_library("payments_section") {
deps = [ deps = [
":credit_card_edit_dialog", ":credit_card_edit_dialog",
...@@ -134,5 +138,8 @@ js_library("password_edit_dialog") { ...@@ -134,5 +138,8 @@ js_library("password_edit_dialog") {
} }
js_library("show_password_behavior") { js_library("show_password_behavior") {
deps = [
":blocking_request_manager",
]
externs_list = [ "$externs_path/passwords_private.js" ] externs_list = [ "$externs_path/passwords_private.js" ]
} }
<link rel="import" href="chrome://resources/html/cr.html">
<script src="blocking_request_manager.js"></script>
// Copyright 2019 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.
/**
* @fileoverview Helper class for making blocking requests that are resolved
* elsewhere in the DOM.
*/
cr.define('settings', function() {
class BlockingRequestManager {
/** @param {Function} makeRequest Function to initiate flow for request. */
constructor(makeRequest) {
this.makeRequest_ = makeRequest;
/**
* @private {Function} callback Provided in requests and called when the
* request is resolved.
*/
this.callback_ = null;
}
/**
* Make a blocking request.
* @param {Function} callback Function to be called if/when the request is
* successfully resolved.
*/
request(callback) {
this.callback_ = callback;
this.makeRequest_();
}
/** Called if/when request is resolved successfully. */
resolve() {
this.callback_();
}
}
return {
BlockingRequestManager: BlockingRequestManager,
};
});
...@@ -6,6 +6,9 @@ ...@@ -6,6 +6,9 @@
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html"> <link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-progress/paper-progress.html"> <link rel="import" href="chrome://resources/polymer/v1_0/paper-progress/paper-progress.html">
<link rel="import" href="../settings_shared_css.html"> <link rel="import" href="../settings_shared_css.html">
<if expr="chromeos">
<link rel="import" href="blocking_request_manager.html">
</if>
<dom-module id="passwords-export-dialog"> <dom-module id="passwords-export-dialog">
<template> <template>
......
...@@ -52,6 +52,11 @@ Polymer({ ...@@ -52,6 +52,11 @@ Polymer({
/** @private */ /** @private */
showErrorDialog_: Boolean, showErrorDialog_: Boolean,
// <if expr="chromeos">
/** @type settings.BlockingRequestManager */
tokenRequestManager: Object
// </if>
}, },
listeners: { listeners: {
...@@ -177,11 +182,22 @@ Polymer({ ...@@ -177,11 +182,22 @@ Polymer({
this.async(() => this.fire('passwords-export-dialog-close')); this.async(() => this.fire('passwords-export-dialog-close'));
}, },
/** @private */
onExportTap_: function() {
// <if expr="chromeos">
this.tokenRequestManager.request(this.exportPasswords_.bind(this));
// </if>
// <if expr="not chromeos">
this.exportPasswords_();
// </if>
},
/** /**
* Fires an event that should trigger the password export process. * Tells the PasswordsPrivate API to export saved passwords in a .csv pending
* security checks.
* @private * @private
*/ */
onExportTap_: function() { exportPasswords_: function() {
this.passwordManager_.exportPasswords(() => { this.passwordManager_.exportPasswords(() => {
if (chrome.runtime.lastError && if (chrome.runtime.lastError &&
chrome.runtime.lastError.message == 'in-progress') { chrome.runtime.lastError.message == 'in-progress') {
......
...@@ -23,6 +23,10 @@ ...@@ -23,6 +23,10 @@
<link rel="import" href="passwords_shared_css.html"> <link rel="import" href="passwords_shared_css.html">
<link rel="import" href="password_list_item.html"> <link rel="import" href="password_list_item.html">
<link rel="import" href="password_manager_proxy.html"> <link rel="import" href="password_manager_proxy.html">
<if expr="chromeos">
<link rel="import" href="../controls/password_prompt_dialog.html">
<link rel="import" href="blocking_request_manager.html">
</if>
<dom-module id="passwords-section"> <dom-module id="passwords-section">
<template> <template>
...@@ -106,6 +110,9 @@ ...@@ -106,6 +110,9 @@
scroll-target="[[subpageScrollTarget]]" risk-selection> scroll-target="[[subpageScrollTarget]]" risk-selection>
<template> <template>
<password-list-item item="[[item]]" tabindex$="[[tabIndex]]" <password-list-item item="[[item]]" tabindex$="[[tabIndex]]"
<if expr="chromeos">
token-request-manager="[[tokenRequestManager_]]"
</if>
first$="[[!index]]" iron-list-tab-index="[[tabIndex]]" first$="[[!index]]" iron-list-tab-index="[[tabIndex]]"
last-focused="{{lastFocused_}}" list-blurred="{{listBlurred_}}"> last-focused="{{lastFocused_}}" list-blurred="{{listBlurred_}}">
</password-list-item> </password-list-item>
...@@ -134,14 +141,27 @@ ...@@ -134,14 +141,27 @@
</cr-action-menu> </cr-action-menu>
<template is="dom-if" if="[[showPasswordsExportDialog_]]" restamp> <template is="dom-if" if="[[showPasswordsExportDialog_]]" restamp>
<passwords-export-dialog <passwords-export-dialog
on-passwords-export-dialog-close="onPasswordsExportDialogClosed_"> <if expr="chromeos">
token-request-manager="[[tokenRequestManager_]]"
</if>
on-passwords-export-dialog-close="onPasswordsExportDialogClosed_">
</passwords-export-dialog> </passwords-export-dialog>
</template> </template>
<template is="dom-if" if="[[showPasswordEditDialog_]]" restamp> <template is="dom-if" if="[[showPasswordEditDialog_]]" restamp>
<password-edit-dialog on-close="onPasswordEditDialogClosed_" <password-edit-dialog on-close="onPasswordEditDialogClosed_"
<if expr="chromeos">
token-request-manager="[[tokenRequestManager_]]"
</if>
item="[[activePassword.item]]"> item="[[activePassword.item]]">
</password-edit-dialog> </password-edit-dialog>
</template> </template>
<if expr="chromeos">
<template is="dom-if" if="[[showPasswordPromptDialog_]]" restamp>
<settings-password-prompt-dialog auth-token="{{authToken_}}"
on-close="onPasswordPromptClosed_">
</settings-password-prompt-dialog>
</template>
</if>
<cr-toast id="undoToast" duration="[[toastDuration_]]"> <cr-toast id="undoToast" duration="[[toastDuration_]]">
<div id="undoLabel">$i18n{passwordDeleted}</div> <div id="undoLabel">$i18n{passwordDeleted}</div>
<paper-button on-click="onUndoButtonTap_"> <paper-button on-click="onUndoButtonTap_">
......
...@@ -122,11 +122,28 @@ Polymer({ ...@@ -122,11 +122,28 @@ Polymer({
/** @private */ /** @private */
listBlurred_: Boolean, listBlurred_: Boolean,
// <if expr="chromeos">
/**
* Auth token for retrieving passwords if required by OS.
* @private
*/
authToken_: {
type: String,
value: '',
observer: 'onAuthTokenChanged_',
},
/** @private */
showPasswordPromptDialog_: Boolean,
/** @private {settings.BlockingRequestManager} */
tokenRequestManager_: Object
// </if>
}, },
listeners: { listeners: {
'password-menu-tap': 'onPasswordMenuTap_', 'password-menu-tap': 'onPasswordMenuTap_',
'export-passwords': 'onExportPasswords_',
}, },
keyBindings: { keyBindings: {
...@@ -184,6 +201,11 @@ Polymer({ ...@@ -184,6 +201,11 @@ Polymer({
// Set the manager. These can be overridden by tests. // Set the manager. These can be overridden by tests.
this.passwordManager_ = PasswordManagerImpl.getInstance(); this.passwordManager_ = PasswordManagerImpl.getInstance();
// <if expr="chromeos">
this.tokenRequestManager_ = new settings.BlockingRequestManager(
() => this.showPasswordPromptDialog_ = true);
// </if>
// Request initial data. // Request initial data.
this.passwordManager_.getSavedPasswordList(setSavedPasswordsListener); this.passwordManager_.getSavedPasswordList(setSavedPasswordsListener);
this.passwordManager_.getExceptionList(setPasswordExceptionsListener); this.passwordManager_.getExceptionList(setPasswordExceptionsListener);
...@@ -219,6 +241,33 @@ Polymer({ ...@@ -219,6 +241,33 @@ Polymer({
} }
}, },
// <if expr="chromeos">
/**
* When |authToken_| changes to a new non-empty value, it means that the
* password-prompt-dialog succeeded in creating a fresh token in the
* quickUnlockPrivate API. Because new tokens can only ever be created
* immediately following a GAIA password check, the passwordsPrivate API can
* now safely grant requests for secure data (i.e. saved passwords) for a
* limited time. This observer resolves the request, triggering a callback
* that requires a fresh auth token to succeed and that was provided to the
* BlockingRequestManager by another DOM element seeking secure data.
*
* @param {string} newToken The newly created auth token. Note that its
* precise value is not relevant here, only the facts that it changed and
* that it is non-empty (i.e. not expired).
* @private
*/
onAuthTokenChanged_: function(newToken) {
if (newToken) {
this.tokenRequestManager_.resolve();
}
},
onPasswordPromptClosed_: function() {
this.showPasswordPromptDialog_ = false;
},
// </if>
/** /**
* Shows the edit password dialog. * Shows the edit password dialog.
* @param {!Event} e * @param {!Event} e
......
<link rel="import" href="chrome://resources/html/polymer.html"> <link rel="import" href="chrome://resources/html/polymer.html">
<if expr="chromeos">
<link rel="import" href="blocking_request_manager.html">
</if>
<script src="show_password_behavior.js"></script> <script src="show_password_behavior.js"></script>
...@@ -16,6 +16,11 @@ const ShowPasswordBehavior = { ...@@ -16,6 +16,11 @@ const ShowPasswordBehavior = {
* @type {!ShowPasswordBehavior.UiEntryWithPassword} * @type {!ShowPasswordBehavior.UiEntryWithPassword}
*/ */
item: Object, item: Object,
// <if expr="chromeos">
/** @type settings.BlockingRequestManager */
tokenRequestManager: Object
// </if>
}, },
/** /**
...@@ -69,13 +74,22 @@ const ShowPasswordBehavior = { ...@@ -69,13 +74,22 @@ const ShowPasswordBehavior = {
onShowPasswordButtonTap_: function() { onShowPasswordButtonTap_: function() {
if (this.item.password) { if (this.item.password) {
this.set('item.password', ''); this.set('item.password', '');
} else { return;
PasswordManagerImpl.getInstance()
.getPlaintextPassword(this.item.entry.id)
.then(password => {
this.set('item.password', password);
});
} }
PasswordManagerImpl.getInstance()
.getPlaintextPassword(this.item.entry.id)
.then(password => {
if (password) {
this.set('item.password', password);
}
// <if expr="chromeos">
if (!password) {
// If no password was found, refresh auth token and retry.
this.tokenRequestManager.request(
this.onShowPasswordButtonTap_.bind(this));
}
// </if>
});
}, },
}; };
......
...@@ -742,12 +742,22 @@ ...@@ -742,12 +742,22 @@
<structure name="IDR_SETTINGS_ADDRESS_EDIT_DIALOG_JS" <structure name="IDR_SETTINGS_ADDRESS_EDIT_DIALOG_JS"
file="autofill_page/address_edit_dialog.js" file="autofill_page/address_edit_dialog.js"
type="chrome_html" /> type="chrome_html" />
<if expr="chromeos">
<structure name="IDR_SETTINGS_BLOCKING_REQUEST_MANAGER_HTML"
file="autofill_page/blocking_request_manager.html"
type="chrome_html" />
<structure name="IDR_SETTINGS_BLOCKING_REQUEST_MANAGER_JS"
file="autofill_page/blocking_request_manager.js"
type="chrome_html" />
</if>
<structure name="IDR_SETTINGS_SHOW_PASSWORD_BEHAVIOR_HTML" <structure name="IDR_SETTINGS_SHOW_PASSWORD_BEHAVIOR_HTML"
file="autofill_page/show_password_behavior.html" file="autofill_page/show_password_behavior.html"
type="chrome_html" /> type="chrome_html"
preprocess="true" />
<structure name="IDR_SETTINGS_SHOW_PASSWORD_BEHAVIOR_JS" <structure name="IDR_SETTINGS_SHOW_PASSWORD_BEHAVIOR_JS"
file="autofill_page/show_password_behavior.js" file="autofill_page/show_password_behavior.js"
type="chrome_html" /> type="chrome_html"
preprocess="true" />
<structure name="IDR_SETTINGS_PASSWORD_LIST_ITEM_HTML" <structure name="IDR_SETTINGS_PASSWORD_LIST_ITEM_HTML"
file="autofill_page/password_list_item.html" file="autofill_page/password_list_item.html"
type="chrome_html" type="chrome_html"
...@@ -763,7 +773,8 @@ ...@@ -763,7 +773,8 @@
type="chrome_html" /> type="chrome_html" />
<structure name="IDR_SETTINGS_PASSWORDS_SECTION_HTML" <structure name="IDR_SETTINGS_PASSWORDS_SECTION_HTML"
file="autofill_page/passwords_section.html" file="autofill_page/passwords_section.html"
type="chrome_html" /> type="chrome_html"
preprocess="true" />
<structure name="IDR_SETTINGS_PASSWORDS_SECTION_JS" <structure name="IDR_SETTINGS_PASSWORDS_SECTION_JS"
file="autofill_page/passwords_section.js" file="autofill_page/passwords_section.js"
type="chrome_html" type="chrome_html"
...@@ -777,10 +788,12 @@ ...@@ -777,10 +788,12 @@
type="chrome_html" /> type="chrome_html" />
<structure name="IDR_SETTINGS_PASSWORDS_EXPORT_DIALOG_HTML" <structure name="IDR_SETTINGS_PASSWORDS_EXPORT_DIALOG_HTML"
file="autofill_page/passwords_export_dialog.html" file="autofill_page/passwords_export_dialog.html"
type="chrome_html" /> type="chrome_html"
preprocess="true" />
<structure name="IDR_SETTINGS_PASSWORDS_EXPORT_DIALOG_JS" <structure name="IDR_SETTINGS_PASSWORDS_EXPORT_DIALOG_JS"
file="autofill_page/passwords_export_dialog.js" file="autofill_page/passwords_export_dialog.js"
type="chrome_html" /> type="chrome_html"
preprocess="true" />
<structure name="IDR_SETTINGS_PAYMENTS_SECTION_HTML" <structure name="IDR_SETTINGS_PAYMENTS_SECTION_HTML"
file="autofill_page/payments_section.html" file="autofill_page/payments_section.html"
type="chrome_html" /> type="chrome_html" />
......
...@@ -340,6 +340,36 @@ TEST_F('CrSettingsPasswordsSectionTest', 'All', function() { ...@@ -340,6 +340,36 @@ TEST_F('CrSettingsPasswordsSectionTest', 'All', function() {
mocha.run(); mocha.run();
}); });
GEN('#if defined(OS_CHROMEOS)');
/**
* Test fixture for CrOS specific behavior in
* chrome/browser/resources/settings/autofill_page/passwords_section.html.
* See http://crbug.com/917178 for details.
* @constructor
* @extends {CrSettingsBrowserTest}
*/
function CrSettingsPasswordsSectionTest_Cros() {}
CrSettingsPasswordsSectionTest_Cros.prototype = {
__proto__: CrSettingsBrowserTest.prototype,
/** @override */
browsePreload: 'chrome://settings/autofill_page/passwords_section.html',
/** @override */
extraLibraries: CrSettingsBrowserTest.prototype.extraLibraries.concat([
'../test_browser_proxy.js',
'passwords_and_autofill_fake_data.js',
'passwords_section_test_cros.js',
'test_password_manager_proxy.js',
]),
};
TEST_F('CrSettingsPasswordsSectionTest_Cros', 'All', function() {
mocha.run();
});
GEN('#endif // defined(OS_CHROMEOS)');
/** /**
* Test fixture for * Test fixture for
* chrome/browser/resources/settings/autofill_page/payments_section.html. * chrome/browser/resources/settings/autofill_page/payments_section.html.
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
* Used to create fake data for both passwords and autofill. * Used to create fake data for both passwords and autofill.
* These sections are related, so it made sense to share this. * These sections are related, so it made sense to share this.
*/ */
function FakeDataMaker() {} function FakeDataMaker() {}
/** /**
...@@ -141,6 +140,96 @@ FakeDataMaker.patternMaker_ = function(pattern, base) { ...@@ -141,6 +140,96 @@ FakeDataMaker.patternMaker_ = function(pattern, base) {
}); });
}; };
/**
* Helper class for creating password-section sub-element from fake data and
* appending them to the document.
*/
class PasswordSectionElementFactory {
/**
* @param {HTMLDocument} document The test's |document| object.
*/
constructor(document) {
this.document = document;
}
/**
* Helper method used to create a password section for the given lists.
* @param {!PasswordManagerProxy} passwordManager
* @param {!Array<!chrome.passwordsPrivate.PasswordUiEntry>} passwordList
* @param {!Array<!chrome.passwordsPrivate.ExceptionEntry>} exceptionList
* @return {!Object}
*/
createPasswordsSection(passwordManager, passwordList, exceptionList) {
// Override the PasswordManagerProxy data for testing.
passwordManager.data.passwords = passwordList;
passwordManager.data.exceptions = exceptionList;
// Create a passwords-section to use for testing.
const passwordsSection = this.document.createElement('passwords-section');
this.document.body.appendChild(passwordsSection);
Polymer.dom.flush();
return passwordsSection;
}
/**
* Helper method used to create a password list item.
* @param {!chrome.passwordsPrivate.PasswordUiEntry} passwordEntry
* @return {!Object}
*/
createPasswordListItem(passwordEntry) {
const passwordListItem = this.document.createElement('password-list-item');
passwordListItem.item = {entry: passwordEntry, password: ''};
this.document.body.appendChild(passwordListItem);
Polymer.dom.flush();
return passwordListItem;
}
/**
* Helper method used to create a password editing dialog.
* @param {!chrome.passwordsPrivate.PasswordUiEntry} passwordEntry
* @return {!Object}
*/
createPasswordEditDialog(passwordEntry) {
const passwordDialog = this.document.createElement('password-edit-dialog');
passwordDialog.item = {entry: passwordEntry, password: ''};
this.document.body.appendChild(passwordDialog);
Polymer.dom.flush();
return passwordDialog;
}
/**
* Helper method used to create an export passwords dialog.
* @return {!Object}
*/
createExportPasswordsDialog(passwordManager) {
passwordManager.requestExportProgressStatus = callback => {
callback(chrome.passwordsPrivate.ExportProgressStatus.NOT_STARTED);
};
passwordManager.addPasswordsFileExportProgressListener = callback => {
passwordManager.progressCallback = callback;
};
passwordManager.removePasswordsFileExportProgressListener = () => {};
passwordManager.exportPasswords = (callback) => {
callback();
};
const dialog = this.document.createElement('passwords-export-dialog');
this.document.body.appendChild(dialog);
Polymer.dom.flush();
if (cr.isChromeOS) {
dialog.tokenRequestManager =
new settings.BlockingRequestManager(function() {
// |this| is expected to be the BlockingRequestManager instance.
this.resolve();
});
}
return dialog;
}
}
/** @constructor */ /** @constructor */
function PasswordManagerExpectations() { function PasswordManagerExpectations() {
this.requested = { this.requested = {
......
// Copyright 2015 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.
/**
* @fileoverview Tests of CrOS specific saved password settings. Note that
* although these tests for only for CrOS, they are testing a CrOS specific
* aspects of the implementation of a browser feature rather than an entirely
* native CrOS feature. See http://crbug.com/917178 for more detail.
*/
cr.define('settings_passwords_section_cros', function() {
suite('PasswordsSection_Cros', function() {
/**
* Promise resolved when a saved password is retrieved.
* @type {Promise}
*/
let requestPromise = null;
/**
* Promise resolved when an auth token request is made.
* @type {Promise}
*/
let passwordPromise = null;
/**
* Implementation of PasswordSectionElementFactory with parameters that help
* tests to track auth token and saved password requests.
*/
class CrosPasswordSectionElementFactory extends
PasswordSectionElementFactory {
/**
* @param {HTMLDocument} document The test's |document| object.
* @param {request: Function} tokenRequestManager Fake for
* BlockingRequestManager provided to subelements of password-section
* that normally have their tokenRequestManager property bound to the
* password section's tokenRequestManager_ property. Note:
* Tests of the password-section element need to use the full
* implementation, which is created by default when the element is
* attached.
* @param {ShowPasswordBehavior.UiEntryWithPassword} passwordItem Wrapper
* for a PasswordUiEntry and the corresponding password.
*/
constructor(document, tokenRequestManager, passwordItem) {
super(document);
this.tokenRequestManager = tokenRequestManager;
this.passwordItem = passwordItem;
}
/** @override */
createPasswordsSection(passwordManager) {
return super.createPasswordsSection(passwordManager, [], []);
}
/** @override */
createPasswordEditDialog() {
return this.decorateShowPasswordElement_('password-edit-dialog');
}
/** @override */
createPasswordListItem() {
return this.decorateShowPasswordElement_('password-list-item');
}
/** @override */
createExportPasswordsDialog(passwordManager) {
return Object.assign(
super.createExportPasswordsDialog(passwordManager),
{tokenRequestManager: this.tokenRequestManager});
}
/**
* Creates an element with ShowPasswordBehavior, decorates it with
* with the testing data provided in the constructor, and attaches it to
* |this.document|.
* @param {string} showPasswordElementName Tag name of a Polymer element
* with ShowPasswordBehavior.
* @return {!HTMLElement} Element decorated with test data.
*/
decorateShowPasswordElement_(showPasswordElementName) {
const element = this.document.createElement(showPasswordElementName);
element.item = this.passwordItem;
element.tokenRequestManager = this.tokenRequestManager;
this.document.body.appendChild(element);
Polymer.dom.flush();
return element;
}
}
function fail() {
throw new Error();
}
/** @type {TestPasswordManagerProxy} */
let passwordManager = null;
/** @type {CrosPasswordSectionElementFactory} */
let elementFactory = null;
setup(function() {
PolymerTest.clearBody();
// Override the PasswordManagerImpl for testing.
passwordManager = new TestPasswordManagerProxy();
PasswordManagerImpl.instance_ = passwordManager;
// Define a fake BlockingRequestManager to track when a token request
// comes in by resolving requestPromise.
let requestManager;
requestPromise = new Promise(resolve => {
requestManager = {request: resolve};
});
/**
* @type {ShowPasswordBehavior.UiEntryWithPassword} Password item (i.e.
* entry with password text) that overrides the password property
* with get/set to track receipt of a saved password and make that
* information available by resolving |passwordPromise|.
*/
let passwordItem;
passwordPromise = new Promise(resolve => {
passwordItem = {
entry: FakeDataMaker.passwordEntry(),
set password(newPassword) {
if (newPassword && newPassword != this.password_) {
resolve(newPassword);
}
this.password_ = newPassword;
},
get password() {
return this.password_;
}
};
});
elementFactory = new CrosPasswordSectionElementFactory(
document, requestManager, passwordItem);
});
test('export passwords button requests auth token', function() {
passwordPromise.then(fail);
const exportDialog =
elementFactory.createExportPasswordsDialog(passwordManager);
exportDialog.$$('#exportPasswordsButton').click();
return requestPromise;
});
test(
'list-item does not request token if it gets password to show',
function() {
requestPromise.then(fail);
const passwordListItem = elementFactory.createPasswordListItem();
passwordManager.setPlaintextPassword('password');
passwordListItem.$$('#showPasswordButton').click();
return passwordPromise;
});
test(
'list-item requests token if it does not get password to show',
function() {
passwordPromise.then(fail);
const passwordListItem = elementFactory.createPasswordListItem();
passwordManager.setPlaintextPassword('');
passwordListItem.$$('#showPasswordButton').click();
return requestPromise;
});
test(
'edit-dialog does not request token if it gets password to show',
function() {
requestPromise.then(fail);
const passwordEditDialog = elementFactory.createPasswordEditDialog();
passwordManager.setPlaintextPassword('password');
passwordEditDialog.$$('#showPasswordButton').click();
return passwordPromise;
});
test(
'edit-dialog requests token if it does not get password to show',
function() {
passwordPromise.then(fail);
const passwordEditDialog = elementFactory.createPasswordEditDialog();
passwordManager.setPlaintextPassword('');
passwordEditDialog.$$('#showPasswordButton').click();
return requestPromise;
});
test('password-prompt-dialog appears on auth token request', function() {
const passwordsSection =
elementFactory.createPasswordsSection(passwordManager);
assertTrue(!passwordsSection.$$('settings-password-prompt-dialog'));
passwordsSection.tokenRequestManager_.request();
Polymer.dom.flush();
assertTrue(!!passwordsSection.$$('settings-password-prompt-dialog'));
});
test(
'password-section resolves request on auth token receipt',
function(done) {
const passwordsSection =
elementFactory.createPasswordsSection(passwordManager);
passwordsSection.tokenRequestManager_.request(done);
passwordsSection.authToken_ = 'auth token';
});
test(
'password-section only triggers callback on most recent request',
function(done) {
const passwordsSection =
elementFactory.createPasswordsSection(passwordManager);
// Make request that SHOULD NOT be resolved.
passwordsSection.tokenRequestManager_.request(fail);
// Make request that should be resolved.
passwordsSection.tokenRequestManager_.request(done);
passwordsSection.authToken_ = 'auth token';
});
});
});
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