Commit 0f30888c authored by Jan Wilken Dörrie's avatar Jan Wilken Dörrie Committed by Commit Bot

[Passwords] Add PasswordCheckReauthenticationHelper

This change adds the PasswordCheckReauthenticationHelper class which
handles reauthentication requests in the password check prior to editing
or viewing a compromised password.

Translation Screenshots:

IDS_PASSWORD_CHECK_LOCKSCREEN_DESCRIPTION_EDIT:
https://storage.cloud.google.com/chromium-translation-screenshots/318cdb4018154327b82c482e6efc3b7e75aaeca7

IDS_PASSWORD_CHECK_LOCKSCREEN_DESCRIPTION_VIEW:

https: //storage.cloud.google.com/chromium-translation-screenshots/202b0c83bad016e36816426996b4ecefce4d2657
Bug: 1114720
Change-Id: I72b197783db6686efd7a1c0bf119c190a0e7ef85
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2352801
Commit-Queue: Jan Wilken Dörrie <jdoerrie@chromium.org>
Reviewed-by: default avatarFriedrich [CET] <fhorschig@chromium.org>
Cr-Commit-Position: refs/heads/master@{#798075}
parent d2a9c8db
......@@ -60,6 +60,7 @@ android_library("internal_java") {
"//chrome/android:chrome_java",
"//chrome/browser/password_check/android:password_check_java_enums",
"//chrome/browser/password_check/android:public_ui_java",
"//chrome/browser/password_manager/android:java",
"//chrome/browser/profiles/android:java",
"//chrome/browser/settings:java",
"//chrome/browser/ui/android/strings:ui_strings_grd",
......@@ -87,6 +88,7 @@ android_library("internal_java") {
"java/src/org/chromium/chrome/browser/password_check/PasswordCheckProperties.java",
"java/src/org/chromium/chrome/browser/password_check/PasswordCheckViewBinder.java",
"java/src/org/chromium/chrome/browser/password_check/PasswordCheckViewHolder.java",
"java/src/org/chromium/chrome/browser/password_check/helper/PasswordCheckReauthenticationHelper.java",
]
annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
resources_package = "org.chromium.chrome.browser.password_check.internal"
......
......@@ -18,6 +18,7 @@ import org.chromium.base.IntentUtils;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.LaunchIntentDispatcher;
import org.chromium.chrome.browser.help.HelpAndFeedback;
import org.chromium.chrome.browser.password_check.helper.PasswordCheckReauthenticationHelper;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
......@@ -39,7 +40,8 @@ class PasswordCheckCoordinator implements PasswordCheckComponentUi, LifecycleObs
private static final String INTENT = "PASSWORD_CHANGE";
private final PasswordCheckFragmentView mFragmentView;
private final PasswordCheckMediator mMediator = new PasswordCheckMediator(this);
private final PasswordCheckReauthenticationHelper mReauthenticationHelper;
private final PasswordCheckMediator mMediator;
private PropertyModel mModel;
/**
......@@ -80,6 +82,11 @@ class PasswordCheckCoordinator implements PasswordCheckComponentUi, LifecycleObs
// TODO(crbug.com/1092444): Ideally, the following replaces the lifecycle event forwarding.
// Figure out why it isn't working and use the following lifecycle observer once it does:
// mFragmentView.getLifecycle().addObserver(this);
mReauthenticationHelper = new PasswordCheckReauthenticationHelper(
mFragmentView.getActivity(), mFragmentView.getParentFragmentManager());
mMediator = new PasswordCheckMediator(this, mReauthenticationHelper);
}
@Override
......@@ -93,6 +100,11 @@ class PasswordCheckCoordinator implements PasswordCheckComponentUi, LifecycleObs
}
}
@Override
public void onResumeFragment() {
mReauthenticationHelper.onReauthenticationMaybeHappened();
}
@Override
public void onDestroyFragment() {
PasswordCheck check = PasswordCheckFactory.getPasswordCheckInstance();
......
......@@ -22,6 +22,9 @@ import android.util.Pair;
import androidx.appcompat.app.AlertDialog;
import org.chromium.chrome.browser.password_check.PasswordCheckComponentUi.ChangePasswordDelegate;
import org.chromium.chrome.browser.password_check.helper.PasswordCheckReauthenticationHelper;
import org.chromium.chrome.browser.password_check.helper.PasswordCheckReauthenticationHelper.ReauthReason;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.PropertyModel;
......@@ -33,11 +36,14 @@ import org.chromium.ui.modelutil.PropertyModel;
class PasswordCheckMediator
implements PasswordCheckCoordinator.CredentialEventHandler, PasswordCheck.Observer {
private final PasswordCheckComponentUi.ChangePasswordDelegate mChangePasswordDelegate;
private final PasswordCheckReauthenticationHelper mReauthenticationHelper;
private PropertyModel mModel;
private PasswordCheckComponentUi.Delegate mDelegate;
PasswordCheckMediator(PasswordCheckCoordinator.ChangePasswordDelegate changePasswordDelegate) {
PasswordCheckMediator(ChangePasswordDelegate changePasswordDelegate,
PasswordCheckReauthenticationHelper reauthenticationHelper) {
mChangePasswordDelegate = changePasswordDelegate;
mReauthenticationHelper = reauthenticationHelper;
}
void initialize(PropertyModel model, PasswordCheckComponentUi.Delegate delegate,
......@@ -135,7 +141,16 @@ class PasswordCheckMediator
@Override
public void onEdit(CompromisedCredential credential) {
// TODO(crbug.com/1114720): Implement.
if (!mReauthenticationHelper.canReauthenticate()) {
// TODO(crbug.com/1114720): Implement Toast explaining to set-up screen lock.
return;
}
mReauthenticationHelper.reauthenticate(ReauthReason.EDIT_PASSWORD,
reauthSucceeded
-> {
// TODO(crbug.com/1114720): Show edit fragment if reauth succeeded.
});
}
@Override
......
// 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.
package org.chromium.chrome.browser.password_check.helper;
import android.content.Context;
import android.view.View;
import androidx.annotation.IntDef;
import androidx.fragment.app.FragmentManager;
import org.chromium.base.Callback;
import org.chromium.chrome.browser.password_check.internal.R;
import org.chromium.chrome.browser.password_manager.settings.ReauthenticationManager;
import org.chromium.chrome.browser.password_manager.settings.ReauthenticationManager.ReauthScope;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A helper to perform a user's reauthentication for a specific {@link ReauthReason}. Only a single
* reauthentication can happen at a given time.
*/
public class PasswordCheckReauthenticationHelper {
/**
* The reason for the reauthentication.
*/
@IntDef({ReauthReason.VIEW_PASSWORD, ReauthReason.EDIT_PASSWORD})
@Retention(RetentionPolicy.SOURCE)
public @interface ReauthReason {
/**
* A reauthentication is required for viewing a password.
*/
int VIEW_PASSWORD = 0;
/**
* A reauthentication is required for editing a password.
*/
int EDIT_PASSWORD = 1;
}
private final Context mContext;
private final FragmentManager mFragmentManager;
private Callback<Boolean> mCallback;
public PasswordCheckReauthenticationHelper(Context context, FragmentManager fragmentManager) {
mContext = context;
mFragmentManager = fragmentManager;
}
public boolean canReauthenticate() {
return ReauthenticationManager.isScreenLockSetUp(mContext);
}
/**
* Asks the user to reauthenticate. Requires {@link #canReauthenticate()}.
* @param reason The {@link ReauthReason} for the reauth.
* @param callback A {@link Callback}. Will invoke {@link Callback#onResult} with whether the
* user passed or dismissed the reauth screen.
*/
public void reauthenticate(@ReauthReason int reason, Callback<Boolean> callback) {
assert canReauthenticate();
assert mCallback == null;
// Invoke the handler immediately if an authentication is still valid.
if (ReauthenticationManager.authenticationStillValid(ReauthScope.ONE_AT_A_TIME)) {
callback.onResult(true);
return;
}
mCallback = callback;
int descriptionId = reason == ReauthReason.VIEW_PASSWORD
? R.string.password_check_lockscreen_description_view
: R.string.password_check_lockscreen_description_edit;
ReauthenticationManager.displayReauthenticationFragment(
descriptionId, View.NO_ID, mFragmentManager, ReauthScope.ONE_AT_A_TIME);
}
/**
* Invoked when a reauthentication might have happened. Invokes {@link Callback#onResult}
* with whether the user passed the reauthentication challenge.
* No-op if {@link #mCallback} is null.
*/
public void onReauthenticationMaybeHappened() {
if (mCallback != null) {
mCallback.onResult(
ReauthenticationManager.authenticationStillValid(ReauthScope.ONE_AT_A_TIME));
mCallback = null;
}
}
}
......@@ -254,6 +254,15 @@
Edit password
</message>
<!-- Lock Screen Fragment strings -->
<message name="IDS_PASSWORD_CHECK_LOCKSCREEN_DESCRIPTION_VIEW" desc="When a user attempts to view a compromised password for a particular website, Chrome launches a lock screen to verify the user's identity and displays the following explanation.">
Unlock to view password
</message>
<message name="IDS_PASSWORD_CHECK_LOCKSCREEN_DESCRIPTION_EDIT" desc="When a user attempts to edit a compromised password for a particular website, Chrome launches a lock screen to verify the user's identity and displays the following explanation.">
Unlock to edit password
</message>
<!-- Utility string to show the timestamp -->
<message name="IDS_PASSWORD_CHECK_JUST_NOW" desc="A time label for a check that happened just now. For example 'Checked passwords · Just now'">
Just now
......
......@@ -57,6 +57,11 @@ interface PasswordCheckComponentUi {
*/
void onStartFragment();
/**
* Forwards the signal that the fragment is being resumed.
*/
void onResumeFragment();
/**
* Forwards the signal that the fragment is being destroyed.
*/
......
......@@ -44,6 +44,12 @@ public class PasswordCheckFragmentView extends PreferenceFragmentCompat {
mComponentDelegate.onStartFragment();
}
@Override
public void onResume() {
super.onResume();
mComponentDelegate.onResumeFragment();
}
private @PasswordCheckReferrer int getReferrerFromInstanceStateOrLaunchBundle(
Bundle savedInstanceState) {
if (savedInstanceState != null && savedInstanceState.containsKey(PASSWORD_CHECK_REFERRER)) {
......
......@@ -9,6 +9,8 @@ import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
......@@ -44,9 +46,12 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.password_check.PasswordCheckProperties.ItemType;
import org.chromium.chrome.browser.password_check.helper.PasswordCheckReauthenticationHelper;
import org.chromium.chrome.browser.password_check.helper.PasswordCheckReauthenticationHelper.ReauthReason;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
import org.chromium.ui.modelutil.ListModel;
......@@ -79,6 +84,8 @@ public class PasswordCheckControllerTest {
private PasswordCheckComponentUi.ChangePasswordDelegate mChangePasswordDelegate;
@Mock
private PasswordCheck mPasswordCheck;
@Mock
private PasswordCheckReauthenticationHelper mReauthenticationHelper;
// DO NOT INITIALIZE HERE! The objects would be shared here which leaks state between tests.
private PasswordCheckMediator mMediator;
......@@ -88,7 +95,7 @@ public class PasswordCheckControllerTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
mModel = PasswordCheckProperties.createDefaultModel();
mMediator = new PasswordCheckMediator(mChangePasswordDelegate);
mMediator = new PasswordCheckMediator(mChangePasswordDelegate, mReauthenticationHelper);
PasswordCheckFactory.setPasswordCheckForTesting(mPasswordCheck);
mMediator.initialize(mModel, mDelegate, PasswordCheckReferrer.PASSWORD_SETTINGS);
}
......@@ -158,6 +165,27 @@ public class PasswordCheckControllerTest {
assertRunningHeader(mModel.get(ITEMS).get(0), PROGRESS_UPDATE);
}
@Test
public void testEditTriggersCanReauthenticate() {
mMediator.onEdit(ANA);
verify(mReauthenticationHelper).canReauthenticate();
}
@Test
public void testCannotReauthenticateDoesNothing() {
when(mReauthenticationHelper.canReauthenticate()).thenReturn(false);
mMediator.onEdit(ANA);
verify(mReauthenticationHelper, never()).reauthenticate(anyInt(), any(Callback.class));
}
@Test
public void testCanReauthenticateTriggersReauthenticate() {
when(mReauthenticationHelper.canReauthenticate()).thenReturn(true);
mMediator.onEdit(ANA);
verify(mReauthenticationHelper)
.reauthenticate(eq(ReauthReason.EDIT_PASSWORD), any(Callback.class));
}
@Test
public void testCreatesEntryForExistingCredentials() {
when(mPasswordCheck.getCompromisedCredentials())
......
......@@ -134,7 +134,8 @@ public final class ReauthenticationManager {
* @param descriptionId The resource ID of the string to be displayed to explain the reason
* for the reauthentication.
* @param containerViewId The ID of the container, fragments of which will get replaced with the
* reauthentication prompt. It may be equal to View.NO_ID in tests.
* reauthentication prompt. It may be equal to View.NO_ID in tests or
* when coming from password check.
* @param fragmentManager For putting the lock screen on the transaction stack.
*/
public static void displayReauthenticationFragment(int descriptionId, int containerViewId,
......
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