blob: 337900a431ffda788965b54a1e2f6ae53c560a30 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/actor/tools/attempt_login_tool.h"
#include "base/barrier_closure.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/notimplemented.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/actor/actor_features.h"
#include "chrome/browser/actor/actor_task.h"
#include "chrome/browser/actor/tools/observation_delay_controller.h"
#include "chrome/browser/actor/tools/tool_callbacks.h"
#include "chrome/browser/actor/tools/tool_delegate.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/password_manager/actor_login/actor_login_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/common/actor.mojom-shared.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/actor_webui.mojom.h"
#include "components/favicon/core/favicon_service.h"
#include "components/password_manager/core/browser/actor_login/actor_login_types.h"
#include "components/password_manager/core/browser/features/password_features.h"
#include "content/public/browser/web_contents.h"
#include "ui/gfx/image/image.h"
#include "url/gurl.h"
namespace actor {
namespace {
content::RenderFrameHost& GetPrimaryMainFrameOfTab(tabs::TabHandle tab_handle) {
return *tab_handle.Get()->GetContents()->GetPrimaryMainFrame();
}
mojom::ActionResultCode LoginErrorToActorError(
actor_login::ActorLoginError login_error) {
switch (login_error) {
case actor_login::ActorLoginError::kServiceBusy:
return mojom::ActionResultCode::kLoginTooManyRequests;
case actor_login::ActorLoginError::kInvalidTabInterface:
return mojom::ActionResultCode::kTabWentAway;
case actor_login::ActorLoginError::kFillingNotAllowed:
return mojom::ActionResultCode::kLoginFillingNotAllowed;
case actor_login::ActorLoginError::kFeatureDisabled:
return mojom::ActionResultCode::kLoginFeatureDisabled;
}
}
mojom::ActionResultCode LoginResultToActorResult(
actor_login::LoginStatusResult login_result) {
// TODO(crbug.com/427817201): Re-assess whether all success statuses should
// map to kOk or if differentiation is needed.
switch (login_result) {
case actor_login::LoginStatusResult::kSuccessUsernameAndPasswordFilled:
case actor_login::LoginStatusResult::kSuccessUsernameFilled:
case actor_login::LoginStatusResult::kSuccessPasswordFilled:
return mojom::ActionResultCode::kOk;
case actor_login::LoginStatusResult::kErrorNoSigninForm:
return mojom::ActionResultCode::kLoginNotLoginPage;
case actor_login::LoginStatusResult::kErrorInvalidCredential:
return mojom::ActionResultCode::kLoginNoCredentialsAvailable;
case actor_login::LoginStatusResult::kErrorNoFillableFields:
return mojom::ActionResultCode::kLoginNoFillableFields;
case actor_login::LoginStatusResult::kErrorDeviceReauthRequired:
return mojom::ActionResultCode::kLoginDeviceReauthRequired;
case actor_login::LoginStatusResult::kErrorDeviceReauthFailed:
return mojom::ActionResultCode::kLoginDeviceReauthFailed;
}
}
} // namespace
AttemptLoginTool::AttemptLoginTool(TaskId task_id,
ToolDelegate& tool_delegate,
tabs::TabInterface& tab)
: Tool(task_id, tool_delegate), tab_handle_(tab.GetHandle()) {}
AttemptLoginTool::~AttemptLoginTool() {
// Uploading the quality log on the destruction of the tool.
tabs::TabInterface* tab = tab_handle_.Get();
Profile* profile =
tab ? Profile::FromBrowserContext(tab->GetContents()->GetBrowserContext())
: nullptr;
// TODO(crbug.com/459397449): Update where the log is uploaded and
// send a pointer to the profile/service when creating the log instead
// of at the moment of uploading.
if (!profile) {
return;
}
OptimizationGuideKeyedService* opt_guide_service =
OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
if (opt_guide_service &&
base::FeatureList::IsEnabled(
password_manager::features::kActorLoginQualityLogs)) {
// TODO(crbug.com/459393643): Add a check for filtering out logs of
// enterprise users.
quality_logger_.UploadFinalLog(
opt_guide_service->GetModelQualityLogsUploaderService());
}
}
void AttemptLoginTool::Validate(ToolCallback callback) {
if (!base::FeatureList::IsEnabled(password_manager::features::kActorLogin)) {
PostResponseTask(std::move(callback),
MakeResult(mojom::ActionResultCode::kToolUnknown));
return;
}
PostResponseTask(std::move(callback), MakeOkResult());
}
void AttemptLoginTool::Invoke(ToolCallback callback) {
tabs::TabInterface* tab = tab_handle_.Get();
if (!tab) {
PostResponseTask(std::move(callback),
MakeResult(mojom::ActionResultCode::kTabWentAway));
return;
}
content::RenderFrameHost* main_rfh =
tab->GetContents()->GetPrimaryMainFrame();
main_rfh_token_ = main_rfh->GetGlobalFrameToken();
invoke_callback_ = std::move(callback);
// First check if there is a user selected credential for the current request
// origin. If so, use it immediately.
const url::Origin& current_origin = main_rfh->GetLastCommittedOrigin();
const std::optional<ToolDelegate::CredentialWithPermission>
user_selected_credential_and_pemission =
tool_delegate().GetUserSelectedCredential(current_origin);
if (user_selected_credential_and_pemission.has_value()) {
const bool should_store_permission =
user_selected_credential_and_pemission->permission_duration ==
webui::mojom::UserGrantedPermissionDuration::kAlwaysAllow;
GetActorLoginService().AttemptLogin(
tab, user_selected_credential_and_pemission->credential,
should_store_permission, quality_logger_.AsWeakPtr(),
base::BindOnce(&AttemptLoginTool::OnAttemptLogin,
weak_ptr_factory_.GetWeakPtr(),
user_selected_credential_and_pemission->credential,
should_store_permission));
return;
}
GetActorLoginService().GetCredentials(
tab, quality_logger_.AsWeakPtr(),
base::BindOnce(&AttemptLoginTool::OnGetCredentials,
weak_ptr_factory_.GetWeakPtr()));
}
void AttemptLoginTool::OnGetCredentials(
actor_login::CredentialsOrError credentials) {
if (!credentials.has_value()) {
PostResponseTask(std::move(invoke_callback_),
MakeResult(LoginErrorToActorError(credentials.error())));
return;
}
credentials_ = std::move(credentials.value());
if (credentials_.empty()) {
PostResponseTask(
std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kLoginNoCredentialsAvailable));
return;
}
if (base::FeatureList::IsEnabled(
actor::kGlicEnableAutoLoginPersistedPermissions)) {
const auto it_persistent_permission =
std::find_if(credentials_.begin(), credentials_.end(),
[](const actor_login::Credential& cred) {
return cred.has_persistent_permission;
});
if (it_persistent_permission != credentials_.end()) {
OnCredentialSelected(webui::mojom::SelectCredentialDialogResponse::New(
task_id().value(), /*error_reason=*/std::nullopt,
webui::mojom::UserGrantedPermissionDuration::kAlwaysAllow,
it_persistent_permission->id.value()));
return;
}
}
std::erase_if(credentials_, [](const actor_login::Credential& cred) {
return !cred.immediatelyAvailableToLogin;
});
if (credentials_.empty()) {
if (base::FeatureList::IsEnabled(
password_manager::features::kActorLoginGetCredentialsNoLoginForm)) {
// Saved credentials exist, but none are available for login, which
// means that this is not a signin page.
PostResponseTask(std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kLoginNotLoginPage));
} else {
// Don't differentiate between no saved credentials and no login form if
// the flag isn't enabled.
PostResponseTask(
std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kLoginNoCredentialsAvailable));
}
return;
}
tabs::TabInterface* tab = tab_handle_.Get();
if (!tab) {
PostResponseTask(std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kTabWentAway));
return;
}
// Unless the flag is enabled, always auto-select the first credential, which
// is the credential that is most likely to be the correct one.
if (base::FeatureList::IsEnabled(actor::kGlicEnableAutoLoginDialogs)) {
FetchIcons();
} else {
// The task ID doesn't matter here because the task ID check is already
// done at this point.
auto response = webui::mojom::SelectCredentialDialogResponse::New();
response->selected_credential_id = credentials_[0].id.value();
OnCredentialSelected(std::move(response));
}
}
void AttemptLoginTool::FetchIcons() {
favicon::FaviconService* favicon_service =
tool_delegate().GetFaviconService();
if (!favicon_service) {
// If there is no favicon service, just proceed without favicons.
tool_delegate().PromptToSelectCredential(
credentials_,
/*icons=*/{},
base::BindOnce(&AttemptLoginTool::OnCredentialSelected,
weak_ptr_factory_.GetWeakPtr()));
return;
}
base::flat_set<GURL> unique_sites;
for (const auto& cred : credentials_) {
if (!cred.source_site_or_app.empty()) {
unique_sites.insert(GURL(cred.source_site_or_app));
}
}
// OnAllIconsFetched is called immediately if unique_sites is empty.
base::RepeatingClosure barrier = base::BarrierClosure(
unique_sites.size(), base::BindOnce(&AttemptLoginTool::OnAllIconsFetched,
weak_ptr_factory_.GetWeakPtr()));
favicon_requests_tracker_ =
std::vector<base::CancelableTaskTracker>(unique_sites.size());
size_t i = 0u;
for (const GURL& site : unique_sites) {
favicon_service->GetFaviconImageForPageURL(
site,
base::BindOnce(&AttemptLoginTool::OnIconFetched,
weak_ptr_factory_.GetWeakPtr(), barrier, site),
&favicon_requests_tracker_[i]);
++i;
}
}
void AttemptLoginTool::OnIconFetched(
base::RepeatingClosure barrier,
GURL site,
const favicon_base::FaviconImageResult& result) {
if (!result.image.IsEmpty()) {
fetched_icons_[site.GetWithEmptyPath().spec()] = result.image;
}
barrier.Run();
}
void AttemptLoginTool::OnAllIconsFetched() {
tool_delegate().PromptToSelectCredential(
credentials_, fetched_icons_,
base::BindOnce(&AttemptLoginTool::OnCredentialSelected,
weak_ptr_factory_.GetWeakPtr()));
}
void AttemptLoginTool::OnCredentialSelected(
webui::mojom::SelectCredentialDialogResponsePtr response) {
std::optional<actor_login::Credential> selected_credential;
std::vector<actor_login::Credential> credentials = std::move(credentials_);
if (response->error_reason ==
webui::mojom::SelectCredentialDialogErrorReason::
kDialogPromiseNoSubscriber) {
VLOG(1) << "selectCredentialDialogRequestHandler() has no subscriber. "
"The web client is likely not set up correctly.";
} else if (response->selected_credential_id.has_value()) {
auto it = std::find_if(
credentials.begin(), credentials.end(),
[&](const actor_login::Credential& credential) {
return credential.id ==
actor_login::Credential::Id(*response->selected_credential_id);
});
if (it != credentials.end()) {
selected_credential = *it;
} else {
VLOG(1) << "Selected credential id " << *response->selected_credential_id
<< " not found in the credentials list.";
}
} else {
quality_logger_.SetPermissionPicked(
optimization_guide::proto::
ActorLoginQuality_PermissionOption_TASK_STOPPED);
VLOG(2) << "SelectCredentialDialogResponse has no selected "
"credential id.";
}
if (!selected_credential.has_value()) {
// We don't need to distinguish between no credentials being available and a
// user declining the usage of a credential.
PostResponseTask(
std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kLoginNoCredentialsAvailable));
return;
}
if (response->permission_duration.has_value()) {
switch (response->permission_duration.value()) {
case webui::mojom::UserGrantedPermissionDuration::kOneTime:
quality_logger_.SetPermissionPicked(
optimization_guide::proto::
ActorLoginQuality_PermissionOption_ALLOW_ONCE);
break;
case webui::mojom::UserGrantedPermissionDuration::kAlwaysAllow:
quality_logger_.SetPermissionPicked(
optimization_guide::proto::
ActorLoginQuality_PermissionOption_ALWAYS_ALLOW);
break;
}
} else {
quality_logger_.SetPermissionPicked(
optimization_guide::proto::ActorLoginQuality_PermissionOption_UNKNOWN);
}
webui::mojom::UserGrantedPermissionDuration permission_duration =
response->permission_duration.value_or(
webui::mojom::UserGrantedPermissionDuration::kOneTime);
tool_delegate().SetUserSelectedCredential(
ToolDelegate::CredentialWithPermission(*selected_credential,
permission_duration),
base::BindOnce(&AttemptLoginTool::OnCredentialCachingDone,
weak_ptr_factory_.GetWeakPtr(), *selected_credential,
permission_duration));
}
void AttemptLoginTool::OnCredentialCachingDone(
actor_login::Credential selected_credential,
webui::mojom::UserGrantedPermissionDuration permission_duration) {
tabs::TabInterface* tab = tab_handle_.Get();
if (!tab) {
PostResponseTask(std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kTabWentAway));
return;
}
if (main_rfh_token_ !=
tab->GetContents()->GetPrimaryMainFrame()->GetGlobalFrameToken()) {
// Don't proceed with the login attempt, if the page changed while we were
// waiting for credential selection.
PostResponseTask(
std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kLoginPageChangedDuringSelection));
return;
}
const bool should_store_permission =
permission_duration ==
webui::mojom::UserGrantedPermissionDuration::kAlwaysAllow;
GetActorLoginService().AttemptLogin(
tab, selected_credential, should_store_permission,
quality_logger_.AsWeakPtr(),
base::BindOnce(&AttemptLoginTool::OnAttemptLogin,
weak_ptr_factory_.GetWeakPtr(), selected_credential,
should_store_permission));
}
void AttemptLoginTool::OnAttemptLogin(
actor_login::Credential selected_credential,
bool should_store_permission,
actor_login::LoginStatusResultOrError login_status) {
if (!login_status.has_value()) {
PostResponseTask(std::move(invoke_callback_),
MakeResult(LoginErrorToActorError(login_status.error())));
return;
}
if (base::FeatureList::IsEnabled(
password_manager::features::kActorLoginReauthTaskRefocus) &&
login_status.value() ==
actor_login::LoginStatusResult::kErrorDeviceReauthRequired) {
if (!tab_handle_.Get()) {
PostResponseTask(std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kTabWentAway));
return;
}
credential_awaiting_task_focus_ = {selected_credential,
should_store_permission};
ObserveTabToAwaitFocus();
tool_delegate().InterruptFromTool();
return;
}
mojom::ActionResultCode code = LoginResultToActorResult(login_status.value());
PostResponseTask(std::move(invoke_callback_),
IsOk(code) ? MakeOkResult() : MakeResult(code));
}
void AttemptLoginTool::OnWillDetach(tabs::TabInterface* tab,
tabs::TabInterface::DetachReason reason) {
if (reason == tabs::TabInterface::DetachReason::kDelete &&
credential_awaiting_task_focus_.has_value()) {
PostResponseTask(std::move(invoke_callback_),
MakeResult(mojom::ActionResultCode::kTabWentAway));
}
}
void AttemptLoginTool::HandleTabActivatedChange(tabs::TabInterface* tab) {
MaybeRetryCredentialNeedingFocus();
}
void AttemptLoginTool::HandleWindowActivatedChange(
BrowserWindowInterface* browser_window) {
MaybeRetryCredentialNeedingFocus();
}
void AttemptLoginTool::ObserveTabToAwaitFocus() {
tabs::TabInterface* tab = tab_handle_.Get();
CHECK(tab);
will_detach_subscription_ = tab->RegisterWillDetach(base::BindRepeating(
&AttemptLoginTool::OnWillDetach, base::Unretained(this)));
tab_did_activate_subscription_ = tab->RegisterDidActivate(base::BindRepeating(
&AttemptLoginTool::HandleTabActivatedChange, base::Unretained(this)));
BrowserWindowInterface* browser_window = tab->GetBrowserWindowInterface();
// TODO(mcnee): Should we update the window subscription if the tab is moved?
// The tab would probably be focused first which would cause us to stop
// observing anyway.
window_did_become_active_subscription_ =
browser_window->RegisterDidBecomeActive(
base::BindRepeating(&AttemptLoginTool::HandleWindowActivatedChange,
base::Unretained(this)));
}
void AttemptLoginTool::StopObservingTab() {
will_detach_subscription_ = {};
tab_did_activate_subscription_ = {};
window_did_become_active_subscription_ = {};
}
void AttemptLoginTool::MaybeRetryCredentialNeedingFocus() {
if (!credential_awaiting_task_focus_.has_value()) {
return;
}
tabs::TabInterface* tab = tab_handle_.Get();
CHECK(tab);
BrowserWindowInterface* browser_window = tab->GetBrowserWindowInterface();
// Note that this is more specific than the conditions checked in
// `ActorLoginDelegateImpl::IsTaskInFocus`, but for simplicity we check for
// the specific tab being activated, since the task nudge will take the user
// there anyway.
if (!browser_window->IsActive() || !tab->IsActivated()) {
return;
}
StopObservingTab();
tool_delegate().UninterruptFromTool();
GetActorLoginService().AttemptLogin(
tab, credential_awaiting_task_focus_->first,
credential_awaiting_task_focus_->second, quality_logger_.AsWeakPtr(),
base::BindOnce(&AttemptLoginTool::OnAttemptLogin,
weak_ptr_factory_.GetWeakPtr(),
credential_awaiting_task_focus_->first,
credential_awaiting_task_focus_->second));
}
std::string AttemptLoginTool::DebugString() const {
return "AttemptLoginTool";
}
std::string AttemptLoginTool::JournalEvent() const {
return "AttemptLogin";
}
std::unique_ptr<ObservationDelayController>
AttemptLoginTool::GetObservationDelayer(
ObservationDelayController::PageStabilityConfig page_stability_config) {
return std::make_unique<ObservationDelayController>(
GetPrimaryMainFrameOfTab(tab_handle_), task_id(), journal(),
page_stability_config);
}
void AttemptLoginTool::UpdateTaskBeforeInvoke(ActorTask& task,
ToolCallback callback) const {
task.AddTab(tab_handle_, std::move(callback));
}
tabs::TabHandle AttemptLoginTool::GetTargetTab() const {
return tab_handle_;
}
actor_login::ActorLoginService& AttemptLoginTool::GetActorLoginService() {
return tool_delegate().GetActorLoginService();
}
} // namespace actor