diff options
Diffstat (limited to 'chromium/chrome')
179 files changed, 32546 insertions, 0 deletions
diff --git a/chromium/chrome/browser/gcm/COMMON_METADATA b/chromium/chrome/browser/gcm/COMMON_METADATA new file mode 100644 index 00000000000..777cdb6a192 --- /dev/null +++ b/chromium/chrome/browser/gcm/COMMON_METADATA @@ -0,0 +1,4 @@ +monorail: { + component: "Services>CloudMessaging" +} +team_email: "platform-capabilities@chromium.org" diff --git a/chromium/chrome/browser/gcm/DIR_METADATA b/chromium/chrome/browser/gcm/DIR_METADATA new file mode 100644 index 00000000000..66fe8683773 --- /dev/null +++ b/chromium/chrome/browser/gcm/DIR_METADATA @@ -0,0 +1 @@ +mixins: "//chrome/browser/gcm/COMMON_METADATA" diff --git a/chromium/chrome/browser/gcm/OWNERS b/chromium/chrome/browser/gcm/OWNERS new file mode 100644 index 00000000000..06f7d3cbe88 --- /dev/null +++ b/chromium/chrome/browser/gcm/OWNERS @@ -0,0 +1,4 @@ +dimich@chromium.org +fgorski@chromium.org +jianli@chromium.org +peter@chromium.org diff --git a/chromium/chrome/browser/gcm/gcm_product_util.cc b/chromium/chrome/browser/gcm/gcm_product_util.cc new file mode 100644 index 00000000000..cb2fa486c11 --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_product_util.cc @@ -0,0 +1,57 @@ +// Copyright 2016 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. + +#include "chrome/browser/gcm/gcm_product_util.h" + +#include "base/strings/string_piece.h" +#include "base/strings/string_util.h" +#include "chrome/common/chrome_version.h" +#include "chrome/common/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_service.h" +#include "components/version_info/version_info.h" + +namespace gcm { + +namespace { + +std::string ToLowerAlphaNum(base::StringPiece in) { + std::string out; + out.reserve(in.size()); + for (char ch : in) { + if (base::IsAsciiAlpha(ch) || base::IsAsciiDigit(ch)) + out.push_back(base::ToLowerASCII(ch)); + } + return out; +} + +} // namespace + +std::string GetProductCategoryForSubtypes(PrefService* prefs) { + std::string product_category_for_subtypes = + prefs->GetString(prefs::kGCMProductCategoryForSubtypes); + if (!product_category_for_subtypes.empty()) + return product_category_for_subtypes; + + std::string product = ToLowerAlphaNum(PRODUCT_SHORTNAME_STRING); + std::string ns = product == "chromium" ? "org" : "com"; + std::string platform = ToLowerAlphaNum(version_info::GetOSType()); + product_category_for_subtypes = ns + '.' + product + '.' + platform; + + prefs->SetString(prefs::kGCMProductCategoryForSubtypes, + product_category_for_subtypes); + return product_category_for_subtypes; +} + +void RegisterPrefs(PrefRegistrySimple* registry) { + registry->RegisterStringPref(prefs::kGCMProductCategoryForSubtypes, + std::string() /* default_value */); +} + +void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { + RegisterPrefs(registry); +} + +} // namespace gcm diff --git a/chromium/chrome/browser/gcm/gcm_product_util.h b/chromium/chrome/browser/gcm/gcm_product_util.h new file mode 100644 index 00000000000..f91c869b310 --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_product_util.h @@ -0,0 +1,30 @@ +// Copyright 2016 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. + +#ifndef CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_ +#define CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_ + +#include <string> + +class PrefRegistrySimple; +class PrefService; + +namespace user_prefs { +class PrefRegistrySyncable; +} + +namespace gcm { + +// Returns a string like "com.chrome.macosx" that should be used as the GCM +// category when an app_id is sent as a subtype instead of as a category. This +// is generated once, then remains fixed forever (even if the product name +// changes), since it must match existing Instance ID tokens. +std::string GetProductCategoryForSubtypes(PrefService* prefs); + +void RegisterPrefs(PrefRegistrySimple* registry); +void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + +} // namespace gcm + +#endif // CHROME_BROWSER_GCM_GCM_PRODUCT_UTIL_H_ diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc b/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc new file mode 100644 index 00000000000..b3207963fdb --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_profile_service_factory.cc @@ -0,0 +1,174 @@ +// Copyright (c) 2013 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. + +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include <memory> + +#include "base/bind.h" +#include "base/no_destructor.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/thread_pool.h" +#include "build/build_config.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_key.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/offline_pages/buildflags/buildflags.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/browser/storage_partition.h" +#include "services/network/public/mojom/network_context.mojom.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/gcm/gcm_product_util.h" +#include "chrome/common/channel_info.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "content/public/browser/browser_context.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#endif + +#if BUILDFLAG(ENABLE_OFFLINE_PAGES) +#include "chrome/browser/offline_pages/prefetch/prefetch_service_factory.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/offline_pages/core/offline_page_feature.h" +#include "components/offline_pages/core/prefetch/prefetch_gcm_app_handler.h" +#include "components/offline_pages/core/prefetch/prefetch_service.h" +#endif + +namespace gcm { + +namespace { + +#if !defined(OS_ANDROID) +// Requests a ProxyResolvingSocketFactory on the UI thread. Note that a WeakPtr +// of GCMProfileService is needed to detect when the KeyedService shuts down, +// and avoid calling into |profile| which might have also been destroyed. +void RequestProxyResolvingSocketFactoryOnUIThread( + Profile* profile, + base::WeakPtr<GCMProfileService> service, + mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory> + receiver) { + if (!service) + return; + network::mojom::NetworkContext* network_context = + profile->GetDefaultStoragePartition()->GetNetworkContext(); + network_context->CreateProxyResolvingSocketFactory(std::move(receiver)); +} + +// A thread-safe wrapper to request a ProxyResolvingSocketFactory. +void RequestProxyResolvingSocketFactory( + Profile* profile, + base::WeakPtr<GCMProfileService> service, + mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory> + receiver) { + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(&RequestProxyResolvingSocketFactoryOnUIThread, profile, + std::move(service), std::move(receiver))); +} +#endif + +BrowserContextKeyedServiceFactory::TestingFactory& GetTestingFactory() { + static base::NoDestructor<BrowserContextKeyedServiceFactory::TestingFactory> + testing_factory; + return *testing_factory; +} + +} // namespace + +GCMProfileServiceFactory::ScopedTestingFactoryInstaller:: + ScopedTestingFactoryInstaller(TestingFactory testing_factory) { + DCHECK(!GetTestingFactory()); + GetTestingFactory() = std::move(testing_factory); +} + +GCMProfileServiceFactory::ScopedTestingFactoryInstaller:: + ~ScopedTestingFactoryInstaller() { + GetTestingFactory() = BrowserContextKeyedServiceFactory::TestingFactory(); +} + +// static +GCMProfileService* GCMProfileServiceFactory::GetForProfile( + content::BrowserContext* profile) { + // GCM is not supported in incognito mode. + if (profile->IsOffTheRecord()) + return NULL; + + return static_cast<GCMProfileService*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +GCMProfileServiceFactory* GCMProfileServiceFactory::GetInstance() { + static base::NoDestructor<GCMProfileServiceFactory> instance; + return instance.get(); +} + +GCMProfileServiceFactory::GCMProfileServiceFactory() + : BrowserContextKeyedServiceFactory( + "GCMProfileService", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +#if BUILDFLAG(ENABLE_OFFLINE_PAGES) + DependsOn(offline_pages::PrefetchServiceFactory::GetInstance()); +#endif // BUILDFLAG(ENABLE_OFFLINE_PAGES) +} + +GCMProfileServiceFactory::~GCMProfileServiceFactory() { +} + +KeyedService* GCMProfileServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + DCHECK(!profile->IsOffTheRecord()); + + TestingFactory& testing_factory = GetTestingFactory(); + if (testing_factory) + return testing_factory.Run(context).release(); + + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner( + base::ThreadPool::CreateSequencedTaskRunner( + {base::MayBlock(), base::TaskPriority::BEST_EFFORT, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})); + std::unique_ptr<GCMProfileService> service; +#if defined(OS_ANDROID) + service = std::make_unique<GCMProfileService>(profile->GetPath(), + blocking_task_runner); +#else + service = std::make_unique<GCMProfileService>( + profile->GetPrefs(), profile->GetPath(), + base::BindRepeating(&RequestProxyResolvingSocketFactory, profile), + profile->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(), + content::GetNetworkConnectionTracker(), chrome::GetChannel(), + gcm::GetProductCategoryForSubtypes(profile->GetPrefs()), + IdentityManagerFactory::GetForProfile(profile), + std::make_unique<GCMClientFactory>(), content::GetUIThreadTaskRunner({}), + content::GetIOThreadTaskRunner({}), blocking_task_runner); +#endif +#if BUILDFLAG(ENABLE_OFFLINE_PAGES) + offline_pages::PrefetchService* prefetch_service = + offline_pages::PrefetchServiceFactory::GetForKey( + profile->GetProfileKey()); + if (prefetch_service != nullptr) { + offline_pages::PrefetchGCMHandler* prefetch_gcm_handler = + prefetch_service->GetPrefetchGCMHandler(); + service->driver()->AddAppHandler(prefetch_gcm_handler->GetAppId(), + prefetch_gcm_handler->AsGCMAppHandler()); + } +#endif // BUILDFLAG(ENABLE_OFFLINE_PAGES) + + return service.release(); +} + +content::BrowserContext* GCMProfileServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextOwnInstanceInIncognito(context); +} + +} // namespace gcm diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_factory.h b/chromium/chrome/browser/gcm/gcm_profile_service_factory.h new file mode 100644 index 00000000000..13511e84611 --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_profile_service_factory.h @@ -0,0 +1,57 @@ +// Copyright (c) 2013 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. + +#ifndef CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_ +#define CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_ + +#include "base/no_destructor.h" +#include "components/gcm_driver/system_encryptor.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace gcm { + +class GCMProfileService; + +// Singleton that owns all GCMProfileService and associates them with +// Profiles. +class GCMProfileServiceFactory : public BrowserContextKeyedServiceFactory { + public: + static GCMProfileService* GetForProfile(content::BrowserContext* profile); + static GCMProfileServiceFactory* GetInstance(); + + // Helper registering a testing factory. Needs to be instantiated before the + // factory is accessed in your test, and deallocated after the last access. + // Usually this is achieved by putting this object as the first member in + // your test fixture. + class ScopedTestingFactoryInstaller { + public: + explicit ScopedTestingFactoryInstaller(TestingFactory testing_factory); + + ScopedTestingFactoryInstaller(const ScopedTestingFactoryInstaller&) = + delete; + ScopedTestingFactoryInstaller& operator=( + const ScopedTestingFactoryInstaller&) = delete; + + ~ScopedTestingFactoryInstaller(); + }; + + GCMProfileServiceFactory(const GCMProfileServiceFactory&) = delete; + GCMProfileServiceFactory& operator=(const GCMProfileServiceFactory&) = delete; + + private: + friend base::NoDestructor<GCMProfileServiceFactory>; + + GCMProfileServiceFactory(); + ~GCMProfileServiceFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +} // namespace gcm + +#endif // CHROME_BROWSER_GCM_GCM_PROFILE_SERVICE_FACTORY_H_ diff --git a/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc b/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc new file mode 100644 index 00000000000..2a13932a454 --- /dev/null +++ b/chromium/chrome/browser/gcm/gcm_profile_service_unittest.cc @@ -0,0 +1,269 @@ +// Copyright 2014 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. + +#include <memory> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/thread_pool.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/gcm/gcm_product_util.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/common/channel_info.h" +#include "chrome/test/base/testing_profile.h" +#include "components/gcm_driver/fake_gcm_app_handler.h" +#include "components/gcm_driver/fake_gcm_client.h" +#include "components/gcm_driver/fake_gcm_client_factory.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/gcm_client_factory.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/test/browser_task_environment.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/mojom/network_context.mojom.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chromeos/dbus/concierge/concierge_client.h" +#endif + +namespace gcm { + +namespace { + +const char kTestAppID[] = "TestApp"; +const char kUserID[] = "user"; + +void RequestProxyResolvingSocketFactoryOnUIThread( + Profile* profile, + base::WeakPtr<gcm::GCMProfileService> service, + mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory> + receiver) { + if (!service) + return; + return profile->GetDefaultStoragePartition() + ->GetNetworkContext() + ->CreateProxyResolvingSocketFactory(std::move(receiver)); +} + +void RequestProxyResolvingSocketFactory( + Profile* profile, + base::WeakPtr<gcm::GCMProfileService> service, + mojo::PendingReceiver<network::mojom::ProxyResolvingSocketFactory> + receiver) { + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(&RequestProxyResolvingSocketFactoryOnUIThread, + profile, service, std::move(receiver))); +} + +std::unique_ptr<KeyedService> BuildGCMProfileService( + content::BrowserContext* context) { + Profile* profile = Profile::FromBrowserContext(context); + scoped_refptr<base::SequencedTaskRunner> blocking_task_runner( + base::ThreadPool::CreateSequencedTaskRunner( + {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})); + return std::make_unique<gcm::GCMProfileService>( + profile->GetPrefs(), profile->GetPath(), + base::BindRepeating(&RequestProxyResolvingSocketFactory, profile), + profile->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(), + network::TestNetworkConnectionTracker::GetInstance(), + chrome::GetChannel(), + gcm::GetProductCategoryForSubtypes(profile->GetPrefs()), + IdentityManagerFactory::GetForProfile(profile), + std::unique_ptr<gcm::GCMClientFactory>( + new gcm::FakeGCMClientFactory(content::GetUIThreadTaskRunner({}), + content::GetIOThreadTaskRunner({}))), + content::GetUIThreadTaskRunner({}), content::GetIOThreadTaskRunner({}), + blocking_task_runner); +} + +} // namespace + +class GCMProfileServiceTest : public testing::Test { + public: + GCMProfileServiceTest(const GCMProfileServiceTest&) = delete; + GCMProfileServiceTest& operator=(const GCMProfileServiceTest&) = delete; + + protected: + GCMProfileServiceTest(); + ~GCMProfileServiceTest() override; + + // testing::Test: + void SetUp() override; + void TearDown() override; + + FakeGCMClient* GetGCMClient() const; + + void CreateGCMProfileService(); + + void RegisterAndWaitForCompletion(const std::vector<std::string>& sender_ids); + void UnregisterAndWaitForCompletion(); + void SendAndWaitForCompletion(const OutgoingMessage& message); + + void RegisterCompleted(base::OnceClosure callback, + const std::string& registration_id, + GCMClient::Result result); + void UnregisterCompleted(base::OnceClosure callback, + GCMClient::Result result); + void SendCompleted(base::OnceClosure callback, + const std::string& message_id, + GCMClient::Result result); + + GCMDriver* driver() const { return gcm_profile_service_->driver(); } + std::string registration_id() const { return registration_id_; } + GCMClient::Result registration_result() const { return registration_result_; } + GCMClient::Result unregistration_result() const { + return unregistration_result_; + } + std::string send_message_id() const { return send_message_id_; } + GCMClient::Result send_result() const { return send_result_; } + + private: + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr<TestingProfile> profile_; + raw_ptr<GCMProfileService> gcm_profile_service_; + std::unique_ptr<FakeGCMAppHandler> gcm_app_handler_; + + std::string registration_id_; + GCMClient::Result registration_result_; + GCMClient::Result unregistration_result_; + std::string send_message_id_; + GCMClient::Result send_result_; +}; + +GCMProfileServiceTest::GCMProfileServiceTest() + : gcm_profile_service_(nullptr), + gcm_app_handler_(new FakeGCMAppHandler), + registration_result_(GCMClient::UNKNOWN_ERROR), + send_result_(GCMClient::UNKNOWN_ERROR) {} + +GCMProfileServiceTest::~GCMProfileServiceTest() { +} + +FakeGCMClient* GCMProfileServiceTest::GetGCMClient() const { + return static_cast<FakeGCMClient*>( + gcm_profile_service_->driver()->GetGCMClientForTesting()); +} + +void GCMProfileServiceTest::SetUp() { +#if BUILDFLAG(IS_CHROMEOS_ASH) + chromeos::ConciergeClient::InitializeFake(/*fake_cicerone_client=*/nullptr); +#endif + TestingProfile::Builder builder; + profile_ = builder.Build(); +} + +void GCMProfileServiceTest::TearDown() { + gcm_profile_service_->driver()->RemoveAppHandler(kTestAppID); +#if BUILDFLAG(IS_CHROMEOS_ASH) + profile_.reset(); + chromeos::ConciergeClient::Shutdown(); +#endif +} + +void GCMProfileServiceTest::CreateGCMProfileService() { + gcm_profile_service_ = static_cast<GCMProfileService*>( + GCMProfileServiceFactory::GetInstance()->SetTestingFactoryAndUse( + profile_.get(), base::BindRepeating(&BuildGCMProfileService))); + gcm_profile_service_->driver()->AddAppHandler( + kTestAppID, gcm_app_handler_.get()); +} + +void GCMProfileServiceTest::RegisterAndWaitForCompletion( + const std::vector<std::string>& sender_ids) { + base::RunLoop run_loop; + gcm_profile_service_->driver()->Register( + kTestAppID, sender_ids, + base::BindOnce(&GCMProfileServiceTest::RegisterCompleted, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); +} + +void GCMProfileServiceTest::UnregisterAndWaitForCompletion() { + base::RunLoop run_loop; + gcm_profile_service_->driver()->Unregister( + kTestAppID, + base::BindOnce(&GCMProfileServiceTest::UnregisterCompleted, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); +} + +void GCMProfileServiceTest::SendAndWaitForCompletion( + const OutgoingMessage& message) { + base::RunLoop run_loop; + gcm_profile_service_->driver()->Send( + kTestAppID, kUserID, message, + base::BindOnce(&GCMProfileServiceTest::SendCompleted, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); +} + +void GCMProfileServiceTest::RegisterCompleted( + base::OnceClosure callback, + const std::string& registration_id, + GCMClient::Result result) { + registration_id_ = registration_id; + registration_result_ = result; + std::move(callback).Run(); +} + +void GCMProfileServiceTest::UnregisterCompleted(base::OnceClosure callback, + GCMClient::Result result) { + unregistration_result_ = result; + std::move(callback).Run(); +} + +void GCMProfileServiceTest::SendCompleted(base::OnceClosure callback, + const std::string& message_id, + GCMClient::Result result) { + send_message_id_ = message_id; + send_result_ = result; + std::move(callback).Run(); +} + +TEST_F(GCMProfileServiceTest, RegisterAndUnregister) { + CreateGCMProfileService(); + + std::vector<std::string> sender_ids; + sender_ids.push_back("sender"); + RegisterAndWaitForCompletion(sender_ids); + + std::string expected_registration_id = + FakeGCMClient::GenerateGCMRegistrationID(sender_ids); + EXPECT_EQ(expected_registration_id, registration_id()); + EXPECT_EQ(GCMClient::SUCCESS, registration_result()); + + UnregisterAndWaitForCompletion(); + EXPECT_EQ(GCMClient::SUCCESS, unregistration_result()); +} + +TEST_F(GCMProfileServiceTest, Send) { + CreateGCMProfileService(); + + OutgoingMessage message; + message.id = "1"; + message.data["key1"] = "value1"; + SendAndWaitForCompletion(message); + + EXPECT_EQ(message.id, send_message_id()); + EXPECT_EQ(GCMClient::SUCCESS, send_result()); +} + +} // namespace gcm diff --git a/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc new file mode 100644 index 00000000000..8123d8f04c6 --- /dev/null +++ b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.cc @@ -0,0 +1,60 @@ +// Copyright (c) 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. + +#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" + +#include <memory> + +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "chrome/browser/profiles/profile.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +namespace instance_id { + +// static +InstanceIDProfileService* InstanceIDProfileServiceFactory::GetForProfile( + content::BrowserContext* profile) { + // Instance ID is not supported in incognito mode. + if (profile->IsOffTheRecord()) + return NULL; + + return static_cast<InstanceIDProfileService*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +InstanceIDProfileServiceFactory* +InstanceIDProfileServiceFactory::GetInstance() { + return base::Singleton<InstanceIDProfileServiceFactory>::get(); +} + +InstanceIDProfileServiceFactory::InstanceIDProfileServiceFactory() + : BrowserContextKeyedServiceFactory( + "InstanceIDProfileService", + BrowserContextDependencyManager::GetInstance()) { + // GCM is needed for device ID. + DependsOn(gcm::GCMProfileServiceFactory::GetInstance()); +} + +InstanceIDProfileServiceFactory::~InstanceIDProfileServiceFactory() { +} + +KeyedService* InstanceIDProfileServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + return new InstanceIDProfileService( + gcm::GCMProfileServiceFactory::GetForProfile(profile)->driver(), + profile->IsOffTheRecord()); +} + +content::BrowserContext* +InstanceIDProfileServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextOwnInstanceInIncognito(context); +} + +} // namespace instance_id diff --git a/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h new file mode 100644 index 00000000000..c4505760a3d --- /dev/null +++ b/chromium/chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h @@ -0,0 +1,44 @@ +// Copyright (c) 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. + +#ifndef CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_ +#define CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace instance_id { + +class InstanceIDProfileService; + +// Singleton that owns all InstanceIDProfileService and associates them with +// profiles. +class InstanceIDProfileServiceFactory : + public BrowserContextKeyedServiceFactory { + public: + static InstanceIDProfileService* GetForProfile( + content::BrowserContext* profile); + static InstanceIDProfileServiceFactory* GetInstance(); + + InstanceIDProfileServiceFactory(const InstanceIDProfileServiceFactory&) = + delete; + InstanceIDProfileServiceFactory& operator=( + const InstanceIDProfileServiceFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits<InstanceIDProfileServiceFactory>; + + InstanceIDProfileServiceFactory(); + ~InstanceIDProfileServiceFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +} // namespace instance_id + +#endif // CHROME_BROWSER_GCM_INSTANCE_ID_INSTANCE_ID_PROFILE_SERVICE_FACTORY_H_ diff --git a/chromium/chrome/browser/profiles/incognito_helpers.cc b/chromium/chrome/browser/profiles/incognito_helpers.cc new file mode 100644 index 00000000000..a319237928b --- /dev/null +++ b/chromium/chrome/browser/profiles/incognito_helpers.cc @@ -0,0 +1,28 @@ +// Copyright 2013 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. + +#include "chrome/browser/profiles/incognito_helpers.h" + +#include "chrome/browser/profiles/profile.h" + +namespace chrome { + +content::BrowserContext* GetBrowserContextRedirectedInIncognito( + content::BrowserContext* context) { + return Profile::FromBrowserContext(context)->GetOriginalProfile(); +} + +const content::BrowserContext* GetBrowserContextRedirectedInIncognito( + const content::BrowserContext* context) { + const Profile* profile = Profile::FromBrowserContext( + const_cast<content::BrowserContext*>(context)); + return profile->GetOriginalProfile(); +} + +content::BrowserContext* GetBrowserContextOwnInstanceInIncognito( + content::BrowserContext* context) { + return context; +} + +} // namespace chrome diff --git a/chromium/chrome/browser/profiles/incognito_helpers.h b/chromium/chrome/browser/profiles/incognito_helpers.h new file mode 100644 index 00000000000..e8e76ce5b95 --- /dev/null +++ b/chromium/chrome/browser/profiles/incognito_helpers.h @@ -0,0 +1,29 @@ +// Copyright 2013 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. + +#ifndef CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_ +#define CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_ + +namespace content { +class BrowserContext; +} + +namespace chrome { + +// Returns the original browser context even for Incognito contexts. +content::BrowserContext* GetBrowserContextRedirectedInIncognito( + content::BrowserContext* context); + +// Returns the original browser context even for Incognito contexts. +const content::BrowserContext* GetBrowserContextRedirectedInIncognito( + const content::BrowserContext* context); + +// Returns non-NULL even for Incognito contexts so that a separate +// instance of a service is created for the Incognito context. +content::BrowserContext* GetBrowserContextOwnInstanceInIncognito( + content::BrowserContext* context); + +} // namespace chrome + +#endif // CHROME_BROWSER_PROFILES_INCOGNITO_HELPERS_H_ diff --git a/chromium/chrome/browser/push_messaging/DIR_METADATA b/chromium/chrome/browser/push_messaging/DIR_METADATA new file mode 100644 index 00000000000..a684b81174a --- /dev/null +++ b/chromium/chrome/browser/push_messaging/DIR_METADATA @@ -0,0 +1 @@ +mixins: "//content/browser/push_messaging/COMMON_METADATA" diff --git a/chromium/chrome/browser/push_messaging/OWNERS b/chromium/chrome/browser/push_messaging/OWNERS new file mode 100644 index 00000000000..d09ffef01de --- /dev/null +++ b/chromium/chrome/browser/push_messaging/OWNERS @@ -0,0 +1 @@ +file://content/browser/push_messaging/OWNERS diff --git a/chromium/chrome/browser/push_messaging/budget.proto b/chromium/chrome/browser/push_messaging/budget.proto new file mode 100644 index 00000000000..229ea5370b7 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/budget.proto @@ -0,0 +1,30 @@ +// Copyright 2016 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. + +syntax = "proto2"; + +package budget_service; + +// Chrome requires this. +option optimize_for = LITE_RUNTIME; + +// Next available id: 3 +message Budget { + // The sequence of budget chunks and their expiration times. + repeated BudgetChunk budget = 1; + + // The timestamp of the last time that new engagement budget was awarded. + // This stores the internal value needed to construct a base::Time object. + optional int64 engagement_last_updated = 2; +} + +// Next available id: 3 +message BudgetChunk { + // The amount of budget remaining in this chunk. + optional double amount = 1; + + // The timestamp when the budget expires. This stores the internal value + // needed to construct a base::Time object. + optional int64 expiration = 2; +} diff --git a/chromium/chrome/browser/push_messaging/budget_database.cc b/chromium/chrome/browser/push_messaging/budget_database.cc new file mode 100644 index 00000000000..96b2d36d01d --- /dev/null +++ b/chromium/chrome/browser/push_messaging/budget_database.cc @@ -0,0 +1,400 @@ +// Copyright 2016 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. + +#include "chrome/browser/push_messaging/budget_database.h" + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/task/post_task.h" +#include "base/task/thread_pool.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/push_messaging/budget.pb.h" +#include "components/leveldb_proto/public/proto_database_provider.h" +#include "components/site_engagement/content/site_engagement_score.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/storage_partition.h" +#include "url/gurl.h" +#include "url/origin.h" + +using content::BrowserThread; + +namespace { + +// The default amount of time during which a budget will be valid. +constexpr int kBudgetDurationInDays = 4; + +// The amount of budget that a maximally engaged site should receive per hour. +// For context, silent push messages cost 2 each, so this allows 6 silent push +// messages a day for a fully engaged site. See budget_manager.cc for costs of +// various actions. +constexpr double kMaximumHourlyBudget = 12.0 / 24.0; + +} // namespace + +BudgetState::BudgetState() = default; +BudgetState::BudgetState(const BudgetState& other) = default; +BudgetState::~BudgetState() = default; + +BudgetState& BudgetState::operator=(const BudgetState& other) = default; + +BudgetDatabase::BudgetInfo::BudgetInfo() = default; + +BudgetDatabase::BudgetInfo::BudgetInfo(const BudgetInfo&& other) + : last_engagement_award(other.last_engagement_award) { + chunks = std::move(other.chunks); +} + +BudgetDatabase::BudgetInfo::~BudgetInfo() = default; + +BudgetDatabase::BudgetDatabase(Profile* profile) + : profile_(profile), clock_(base::WrapUnique(new base::DefaultClock)) { + auto* protodb_provider = + profile->GetDefaultStoragePartition()->GetProtoDatabaseProvider(); + // In incognito mode the provider service is not created. + if (!protodb_provider) + return; + + db_ = protodb_provider->GetDB<budget_service::Budget>( + leveldb_proto::ProtoDbType::BUDGET_DATABASE, + profile->GetPath().Append(FILE_PATH_LITERAL("BudgetDatabase")), + base::ThreadPool::CreateSequencedTaskRunner( + {base::MayBlock(), base::TaskPriority::BEST_EFFORT, + base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})); + db_->Init(base::BindOnce(&BudgetDatabase::OnDatabaseInit, + weak_ptr_factory_.GetWeakPtr())); +} + +BudgetDatabase::~BudgetDatabase() = default; + +void BudgetDatabase::GetBudgetDetails(const url::Origin& origin, + GetBudgetCallback callback) { + SyncCache(origin, base::BindOnce(&BudgetDatabase::GetBudgetAfterSync, + weak_ptr_factory_.GetWeakPtr(), origin, + std::move(callback))); +} + +void BudgetDatabase::SpendBudget(const url::Origin& origin, + SpendBudgetCallback callback, + double amount) { + SyncCache(origin, base::BindOnce(&BudgetDatabase::SpendBudgetAfterSync, + weak_ptr_factory_.GetWeakPtr(), origin, + amount, std::move(callback))); +} + +void BudgetDatabase::SetClockForTesting(std::unique_ptr<base::Clock> clock) { + clock_ = std::move(clock); +} + +void BudgetDatabase::OnDatabaseInit(leveldb_proto::Enums::InitStatus status) { + // TODO(harkness): Consider caching the budget database now? + if (status != leveldb_proto::Enums::InitStatus::kOK) + db_.reset(); +} + +bool BudgetDatabase::IsCached(const url::Origin& origin) const { + return budget_map_.find(origin) != budget_map_.end(); +} + +double BudgetDatabase::GetBudget(const url::Origin& origin) const { + double total = 0; + auto iter = budget_map_.find(origin); + if (iter == budget_map_.end()) + return total; + + const BudgetInfo& info = iter->second; + for (const BudgetChunk& chunk : info.chunks) + total += chunk.amount; + return total; +} + +void BudgetDatabase::AddToCache( + const url::Origin& origin, + CacheCallback callback, + bool success, + std::unique_ptr<budget_service::Budget> budget_proto) { + // If the database read failed or there's nothing to add, just return. + if (!success || !budget_proto) { + std::move(callback).Run(success); + return; + } + + // If there were two simultaneous loads, don't overwrite the cache value, + // which might have been updated after the previous load. + if (IsCached(origin)) { + std::move(callback).Run(success); + return; + } + + // Add the data to the cache, converting from the proto format to an STL + // format which is better for removing things from the list. + BudgetInfo& info = budget_map_[origin]; + for (const auto& chunk : budget_proto->budget()) { + info.chunks.emplace_back(chunk.amount(), + base::Time::FromInternalValue(chunk.expiration())); + } + + info.last_engagement_award = + base::Time::FromInternalValue(budget_proto->engagement_last_updated()); + + std::move(callback).Run(success); +} + +void BudgetDatabase::GetBudgetAfterSync(const url::Origin& origin, + GetBudgetCallback callback, + bool success) { + std::vector<BudgetState> predictions; + + // If the database wasn't able to read the information, return the + // failure and an empty predictions array. + if (!success) { + std::move(callback).Run(std::move(predictions)); + return; + } + + // Now, build up the BudgetExpection. This is different from the format + // in which the cache stores the data. The cache stores chunks of budget and + // when that budget expires. The mojo array describes a set of times + // and the budget at those times. + double total = GetBudget(origin); + + // Always add one entry at the front of the list for the total budget now. + { + BudgetState prediction; + prediction.budget_at = total; + prediction.time = clock_->Now().ToJsTime(); + predictions.push_back(prediction); + } + + // Starting with the soonest expiring chunks, add entries for the + // expiration times going forward. + const BudgetChunks& chunks = budget_map_[origin].chunks; + for (const auto& chunk : chunks) { + BudgetState prediction; + total -= chunk.amount; + prediction.budget_at = total; + prediction.time = chunk.expiration.ToJsTime(); + predictions.push_back(prediction); + } + + std::move(callback).Run(std::move(predictions)); +} + +void BudgetDatabase::SpendBudgetAfterSync(const url::Origin& origin, + double amount, + SpendBudgetCallback callback, + bool success) { + if (!success) { + std::move(callback).Run(false /* success */); + return; + } + + // Get the current SES score, to generate UMA. + double score = GetSiteEngagementScoreForOrigin(origin); + + // Walk the list of budget chunks to see if the origin has enough budget. + double total = 0; + BudgetInfo& info = budget_map_[origin]; + for (const BudgetChunk& chunk : info.chunks) + total += chunk.amount; + + if (total < amount) { + UMA_HISTOGRAM_COUNTS_100("PushMessaging.SESForNoBudgetOrigin", score); + std::move(callback).Run(false /* success */); + return; + } else if (total < amount * 2) { + UMA_HISTOGRAM_COUNTS_100("PushMessaging.SESForLowBudgetOrigin", score); + } + + // Walk the chunks and remove enough budget to cover the needed amount. + double bill = amount; + for (auto iter = info.chunks.begin(); iter != info.chunks.end();) { + if (iter->amount > bill) { + iter->amount -= bill; + bill = 0; + break; + } + bill -= iter->amount; + iter = info.chunks.erase(iter); + } + + // There should have been enough budget to cover the entire bill. + DCHECK_EQ(0, bill); + + // Now that the cache is updated, write the data to the database. + WriteCachedValuesToDatabase( + origin, + base::BindOnce(&BudgetDatabase::SpendBudgetAfterWrite, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +// This converts the bool value which is returned from the database to a Mojo +// error type. +void BudgetDatabase::SpendBudgetAfterWrite(SpendBudgetCallback callback, + bool write_successful) { + // TODO(harkness): If the database write fails, the cache will be out of sync + // with the database. Consider ways to mitigate this. + if (!write_successful) { + std::move(callback).Run(false /* success */); + return; + } + std::move(callback).Run(true /* success */); +} + +void BudgetDatabase::WriteCachedValuesToDatabase(const url::Origin& origin, + StoreBudgetCallback callback) { + if (!db_) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), false)); + return; + } + + // Create the data structures that are passed to the ProtoDatabase. + std::unique_ptr< + leveldb_proto::ProtoDatabase<budget_service::Budget>::KeyEntryVector> + entries(new leveldb_proto::ProtoDatabase< + budget_service::Budget>::KeyEntryVector()); + std::unique_ptr<std::vector<std::string>> keys_to_remove( + new std::vector<std::string>()); + + // Each operation can either update the existing budget or remove the origin's + // budget information. + if (IsCached(origin)) { + // Build the Budget proto object. + budget_service::Budget budget; + const BudgetInfo& info = budget_map_[origin]; + for (const auto& chunk : info.chunks) { + budget_service::BudgetChunk* budget_chunk = budget.add_budget(); + budget_chunk->set_amount(chunk.amount); + budget_chunk->set_expiration(chunk.expiration.ToInternalValue()); + } + budget.set_engagement_last_updated( + info.last_engagement_award.ToInternalValue()); + entries->push_back(std::make_pair(origin.Serialize(), budget)); + } else { + // If the origin doesn't exist in the cache, this is a remove operation. + keys_to_remove->push_back(origin.Serialize()); + } + + // Send the updates to the database. + db_->UpdateEntries(std::move(entries), std::move(keys_to_remove), + std::move(callback)); +} + +void BudgetDatabase::SyncCache(const url::Origin& origin, + CacheCallback callback) { + // If the origin isn't already cached, add it to the cache. + if (!IsCached(origin)) { + if (!db_) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), false)); + return; + } + CacheCallback add_callback = base::BindOnce( + &BudgetDatabase::SyncLoadedCache, weak_ptr_factory_.GetWeakPtr(), + origin, std::move(callback)); + db_->GetEntry(origin.Serialize(), + base::BindOnce(&BudgetDatabase::AddToCache, + weak_ptr_factory_.GetWeakPtr(), origin, + std::move(add_callback))); + return; + } + SyncLoadedCache(origin, std::move(callback), true /* success */); +} + +void BudgetDatabase::SyncLoadedCache(const url::Origin& origin, + CacheCallback callback, + bool success) { + if (!success) { + std::move(callback).Run(false /* success */); + return; + } + + // Now, cleanup any expired budget chunks for the origin. + bool needs_write = CleanupExpiredBudget(origin); + + // Get the SES score and add engagement budget for the site. + AddEngagementBudget(origin); + + if (needs_write) + WriteCachedValuesToDatabase(origin, std::move(callback)); + else + std::move(callback).Run(success); +} + +void BudgetDatabase::AddEngagementBudget(const url::Origin& origin) { + // Calculate how much budget should be awarded. The award depends on the + // time elapsed since the last award and the SES score. + // By default, give the origin kBudgetDurationInDays worth of budget, but + // reduce that if budget has already been given during that period. + base::TimeDelta elapsed = base::Days(kBudgetDurationInDays); + if (IsCached(origin)) { + elapsed = clock_->Now() - budget_map_[origin].last_engagement_award; + // Don't give engagement awards for periods less than an hour. + if (elapsed.InHours() < 1) + return; + // Cap elapsed time to the budget duration. + if (elapsed.InDays() > kBudgetDurationInDays) + elapsed = base::Days(kBudgetDurationInDays); + } + + // Get the current SES score, and calculate the hourly budget for that score. + double hourly_budget = kMaximumHourlyBudget * + GetSiteEngagementScoreForOrigin(origin) / + site_engagement::SiteEngagementService::GetMaxPoints(); + + // Update the last_engagement_award to the current time. If the origin wasn't + // already in the map, this adds a new entry for it. + budget_map_[origin].last_engagement_award = clock_->Now(); + + // Add a new chunk of budget for the origin at the default expiration time. + base::Time expiration = clock_->Now() + base::Days(kBudgetDurationInDays); + budget_map_[origin].chunks.emplace_back(elapsed.InHours() * hourly_budget, + expiration); + + // Any time we award engagement budget, which is done at most once an hour + // whenever any budget action is taken, record the budget. + double budget = GetBudget(origin); + UMA_HISTOGRAM_COUNTS_100("PushMessaging.BackgroundBudget", budget); +} + +// Cleans up budget in the cache. Relies on the caller eventually writing the +// cache back to the database. +bool BudgetDatabase::CleanupExpiredBudget(const url::Origin& origin) { + if (!IsCached(origin)) + return false; + + base::Time now = clock_->Now(); + BudgetChunks& chunks = budget_map_[origin].chunks; + auto cleanup_iter = chunks.begin(); + + // This relies on the list of chunks being in timestamp order. + while (cleanup_iter != chunks.end() && cleanup_iter->expiration <= now) + cleanup_iter = chunks.erase(cleanup_iter); + + // If the entire budget is empty now AND there have been no engagements + // in the last kBudgetDurationInDays days, remove this from the cache. + if (chunks.empty() && budget_map_[origin].last_engagement_award < + clock_->Now() - base::Days(kBudgetDurationInDays)) { + budget_map_.erase(origin); + return true; + } + + // Although some things may have expired, there are some chunks still valid. + // Don't write to the DB now, write either when all chunks expire or when the + // origin spends some budget. + return false; +} + +double BudgetDatabase::GetSiteEngagementScoreForOrigin( + const url::Origin& origin) const { + if (profile_->IsOffTheRecord()) + return 0; + + return site_engagement::SiteEngagementService::Get(profile_)->GetScore( + origin.GetURL()); +} diff --git a/chromium/chrome/browser/push_messaging/budget_database.h b/chromium/chrome/browser/push_messaging/budget_database.h new file mode 100644 index 00000000000..0ae20374262 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/budget_database.h @@ -0,0 +1,182 @@ +// Copyright 2016 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_ + +#include <list> +#include <map> +#include <memory> + +#include "base/callback_forward.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "components/leveldb_proto/public/proto_database.h" + +namespace base { +class Clock; +class Time; +} // namespace base + +namespace budget_service { +class Budget; +} + +namespace url { +class Origin; +} + +class Profile; + +// Structure representing the budget at points in time in the future. +struct BudgetState { + BudgetState(); + BudgetState(const BudgetState& other); + ~BudgetState(); + + BudgetState& operator=(const BudgetState& other); + + // Amount of budget that will be available. This should be the lower bound of + // the budget between this time and the previous time. + double budget_at = 0; + + // Time at which the budget is available, in milliseconds since 00:00:00 UTC + // on 1 January 1970, at which the budget_at will be valid. + double time = 0; +}; + +// A class used to asynchronously read and write details of the budget +// assigned to an origin. The class uses an underlying LevelDB. +class BudgetDatabase { + public: + // The default amount of budget that should be spent. + static constexpr double kDefaultAmount = 2.0; + + // Callback for getting a list of all budget chunks. + using GetBudgetCallback = base::OnceCallback<void(std::vector<BudgetState>)>; + + // This is invoked only after the spend has been written to the database. + using SpendBudgetCallback = base::OnceCallback<void(bool success)>; + + // The database_dir specifies the location of the budget information on disk. + explicit BudgetDatabase(Profile* profile); + + BudgetDatabase(const BudgetDatabase&) = delete; + BudgetDatabase& operator=(const BudgetDatabase&) = delete; + + ~BudgetDatabase(); + + // Get the full budget expectation for the origin. This will return a + // sequence of time points and the expected budget at those times. + void GetBudgetDetails(const url::Origin& origin, GetBudgetCallback callback); + + // Spend a fixed (2.0) amount of budget for an origin. The callback indicates + // whether the budget could be spent for the given |origin|. + void SpendBudget(const url::Origin& origin, + SpendBudgetCallback callback, + double amount = kDefaultAmount); + + private: + FRIEND_TEST_ALL_PREFIXES(BudgetDatabaseTest, + DefaultSiteEngagementInIncognitoProfile); + friend class BudgetDatabaseTest; + + // Used to allow tests to change time for testing. + void SetClockForTesting(std::unique_ptr<base::Clock> clock); + + // Holds information about individual pieces of awarded budget. There is a + // one-to-one mapping of these to the chunks in the underlying database. + struct BudgetChunk { + BudgetChunk(double amount, base::Time expiration) + : amount(amount), expiration(expiration) {} + BudgetChunk(const BudgetChunk&) = default; + BudgetChunk& operator=(const BudgetChunk&) = default; + + double amount; + base::Time expiration; + }; + + // Data structure for caching budget information. + using BudgetChunks = std::list<BudgetChunk>; + + // Holds information about the overall budget for a site. This includes the + // time the budget was last incremented, as well as a list of budget chunks + // which have been awarded. + struct BudgetInfo { + BudgetInfo(); + + BudgetInfo(const BudgetInfo&) = delete; + BudgetInfo& operator=(const BudgetInfo&) = delete; + + BudgetInfo(const BudgetInfo&& other); + + ~BudgetInfo(); + + base::Time last_engagement_award; + BudgetChunks chunks; + }; + + // Callback for writing budget values to the database. + using StoreBudgetCallback = base::OnceCallback<void(bool success)>; + + using CacheCallback = base::OnceCallback<void(bool success)>; + + void OnDatabaseInit(leveldb_proto::Enums::InitStatus status); + + bool IsCached(const url::Origin& origin) const; + + double GetBudget(const url::Origin& origin) const; + + void AddToCache(const url::Origin& origin, + CacheCallback callback, + bool success, + std::unique_ptr<budget_service::Budget> budget); + + void GetBudgetAfterSync(const url::Origin& origin, + GetBudgetCallback callback, + bool success); + + void SpendBudgetAfterSync(const url::Origin& origin, + double amount, + SpendBudgetCallback callback, + bool success); + + void SpendBudgetAfterWrite(SpendBudgetCallback callback, bool success); + + void WriteCachedValuesToDatabase(const url::Origin& origin, + StoreBudgetCallback callback); + + void SyncCache(const url::Origin& origin, CacheCallback callback); + void SyncLoadedCache(const url::Origin& origin, + CacheCallback callback, + bool success); + + // Add budget based on engagement with an origin. The method queries for the + // engagement score of the origin, and then calculates when engagement budget + // was last awarded and awards a portion of the score based on that. + // This only writes budget to the cache. + void AddEngagementBudget(const url::Origin& origin); + + bool CleanupExpiredBudget(const url::Origin& origin); + + // Gets the current Site Engagement Score for |origin|. Will return a fixed + // score of zero when |profile_| is off the record. + double GetSiteEngagementScoreForOrigin(const url::Origin& origin) const; + + raw_ptr<Profile> profile_; + + // The database for storing budget information. + std::unique_ptr<leveldb_proto::ProtoDatabase<budget_service::Budget>> db_; + + // Cached data for the origins which have been loaded. + std::map<url::Origin, BudgetInfo> budget_map_; + + // The clock used to vend times. + std::unique_ptr<base::Clock> clock_; + + base::WeakPtrFactory<BudgetDatabase> weak_ptr_factory_{this}; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_BUDGET_DATABASE_H_ diff --git a/chromium/chrome/browser/push_messaging/budget_database_unittest.cc b/chromium/chrome/browser/push_messaging/budget_database_unittest.cc new file mode 100644 index 00000000000..dbf29452424 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/budget_database_unittest.cc @@ -0,0 +1,351 @@ +// Copyright 2016 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. + +#include "chrome/browser/push_messaging/budget_database.h" + +#include <math.h> +#include <vector> + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/run_loop.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/simple_test_clock.h" +#include "chrome/browser/push_messaging/budget.pb.h" +#include "chrome/test/base/testing_profile.h" +#include "components/leveldb_proto/public/proto_database.h" +#include "components/leveldb_proto/public/proto_database_provider.h" +#include "components/site_engagement/content/site_engagement_score.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace { + +// These values mirror the defaults in budget_database.cc +const double kDefaultExpirationInDays = 4; +const double kMaxDailyBudget = 12; + +const double kEngagement = 25; + +const char kTestOrigin[] = "https://example.com"; + +} // namespace + +class BudgetDatabaseTest : public ::testing::Test { + public: + BudgetDatabaseTest() + : success_(false), + db_(&profile_), + origin_(url::Origin::Create(GURL(kTestOrigin))) {} + + void WriteBudgetComplete(base::OnceClosure run_loop_closure, bool success) { + success_ = success; + std::move(run_loop_closure).Run(); + } + + // Spend budget for the origin. + bool SpendBudget(double amount) { + base::RunLoop run_loop; + db_.SpendBudget( + origin(), + base::BindOnce(&BudgetDatabaseTest::WriteBudgetComplete, + base::Unretained(this), run_loop.QuitClosure()), + amount); + run_loop.Run(); + return success_; + } + + void GetBudgetDetailsComplete(base::OnceClosure run_loop_closure, + std::vector<BudgetState> predictions) { + success_ = !predictions.empty(); + prediction_.swap(predictions); + std::move(run_loop_closure).Run(); + } + + // Get the full set of budget predictions for the origin. + void GetBudgetDetails() { + base::RunLoop run_loop; + db_.GetBudgetDetails( + origin(), + base::BindOnce(&BudgetDatabaseTest::GetBudgetDetailsComplete, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); + } + + Profile* profile() { return &profile_; } + BudgetDatabase* database() { return &db_; } + const url::Origin& origin() const { return origin_; } + + // Setup a test clock so that the tests can control time. + base::SimpleTestClock* SetClockForTesting() { + base::SimpleTestClock* clock = new base::SimpleTestClock(); + db_.SetClockForTesting(base::WrapUnique(clock)); + return clock; + } + + void SetSiteEngagementScore(double score) { + site_engagement::SiteEngagementService* service = + site_engagement::SiteEngagementService::Get(&profile_); + service->ResetBaseScoreForURL(GURL(kTestOrigin), score); + } + + protected: + base::HistogramTester* GetHistogramTester() { return &histogram_tester_; } + bool success_; + std::vector<BudgetState> prediction_; + + private: + content::BrowserTaskEnvironment task_environment_; + TestingProfile profile_; + BudgetDatabase db_; + base::HistogramTester histogram_tester_; + const url::Origin origin_; +}; + +TEST_F(BudgetDatabaseTest, GetBudgetNoBudgetOrSES) { + GetBudgetDetails(); + ASSERT_TRUE(success_); + ASSERT_EQ(2U, prediction_.size()); + EXPECT_EQ(0, prediction_[0].budget_at); +} + +TEST_F(BudgetDatabaseTest, AddEngagementBudgetTest) { + base::SimpleTestClock* clock = SetClockForTesting(); + base::Time expiration_time = + clock->Now() + base::Days(kDefaultExpirationInDays); + + // Set the default site engagement. + SetSiteEngagementScore(kEngagement); + + // The budget should include kDefaultExpirationInDays days worth of + // engagement. + double daily_budget = + kMaxDailyBudget * + (kEngagement / site_engagement::SiteEngagementScore::kMaxPoints); + GetBudgetDetails(); + ASSERT_TRUE(success_); + ASSERT_EQ(2U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget * kDefaultExpirationInDays, + prediction_[0].budget_at); + ASSERT_EQ(0, prediction_[1].budget_at); + ASSERT_EQ(expiration_time.ToJsTime(), prediction_[1].time); + + // Advance time 1 day and add more engagement budget. + clock->Advance(base::Days(1)); + GetBudgetDetails(); + + // The budget should now have 1 full share plus 1 daily budget. + ASSERT_TRUE(success_); + ASSERT_EQ(3U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget * (kDefaultExpirationInDays + 1), + prediction_[0].budget_at); + ASSERT_DOUBLE_EQ(daily_budget, prediction_[1].budget_at); + ASSERT_EQ(expiration_time.ToJsTime(), prediction_[1].time); + ASSERT_DOUBLE_EQ(0, prediction_[2].budget_at); + ASSERT_EQ((expiration_time + base::Days(1)).ToJsTime(), prediction_[2].time); + + // Advance time by 59 minutes and check that no engagement budget is added + // since budget should only be added for > 1 hour increments. + clock->Advance(base::Minutes(59)); + GetBudgetDetails(); + + // The budget should be the same as before the attempted add. + ASSERT_TRUE(success_); + ASSERT_EQ(3U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget * (kDefaultExpirationInDays + 1), + prediction_[0].budget_at); +} + +TEST_F(BudgetDatabaseTest, SpendBudgetTest) { + base::SimpleTestClock* clock = SetClockForTesting(); + + // Set the default site engagement. + SetSiteEngagementScore(kEngagement); + + // Intialize the budget with several chunks. + GetBudgetDetails(); + clock->Advance(base::Days(1)); + GetBudgetDetails(); + clock->Advance(base::Days(1)); + GetBudgetDetails(); + + // Spend an amount of budget less than the daily budget. + ASSERT_TRUE(SpendBudget(1)); + GetBudgetDetails(); + + // There should still be three chunks of budget of size daily_budget-1, + // daily_budget, and kDefaultExpirationInDays * daily_budget. + double daily_budget = + kMaxDailyBudget * + (kEngagement / site_engagement::SiteEngagementScore::kMaxPoints); + ASSERT_EQ(4U, prediction_.size()); + ASSERT_DOUBLE_EQ((2 + kDefaultExpirationInDays) * daily_budget - 1, + prediction_[0].budget_at); + ASSERT_DOUBLE_EQ(daily_budget * 2, prediction_[1].budget_at); + ASSERT_DOUBLE_EQ(daily_budget, prediction_[2].budget_at); + ASSERT_DOUBLE_EQ(0, prediction_[3].budget_at); + + // Now spend enough that it will use up the rest of the first chunk and all of + // the second chunk, but not all of the third chunk. + ASSERT_TRUE(SpendBudget((1 + kDefaultExpirationInDays) * daily_budget)); + GetBudgetDetails(); + ASSERT_EQ(2U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget - 1, prediction_[0].budget_at); + + // Validate that the code returns false if SpendBudget tries to spend more + // budget than the origin has. + EXPECT_FALSE(SpendBudget(kEngagement)); + GetBudgetDetails(); + ASSERT_EQ(2U, prediction_.size()); + ASSERT_DOUBLE_EQ(daily_budget - 1, prediction_[0].budget_at); + + // Advance time until the last remaining chunk should be expired, then query + // for the full engagement worth of budget. + clock->Advance(base::Days(kDefaultExpirationInDays + 1)); + EXPECT_TRUE(SpendBudget(daily_budget * kDefaultExpirationInDays)); +} + +// There are times when a device's clock could move backwards in time, either +// due to hardware issues or user actions. Test here to make sure that even if +// time goes backwards and then forwards again, the origin isn't granted extra +// budget. +TEST_F(BudgetDatabaseTest, GetBudgetNegativeTime) { + base::SimpleTestClock* clock = SetClockForTesting(); + + // Set the default site engagement. + SetSiteEngagementScore(kEngagement); + + // Initialize the budget with two chunks. + GetBudgetDetails(); + clock->Advance(base::Days(1)); + GetBudgetDetails(); + + // Save off the budget total. + ASSERT_EQ(3U, prediction_.size()); + double budget = prediction_[0].budget_at; + + // Move the clock backwards in time to before the budget awards. + clock->SetNow(clock->Now() - base::Days(5)); + + // Make sure the budget is the same. + GetBudgetDetails(); + ASSERT_EQ(3U, prediction_.size()); + ASSERT_EQ(budget, prediction_[0].budget_at); + + // Now move the clock back to the original time and check that no extra budget + // is awarded. + clock->SetNow(clock->Now() + base::Days(5)); + GetBudgetDetails(); + ASSERT_EQ(3U, prediction_.size()); + ASSERT_EQ(budget, prediction_[0].budget_at); +} + +TEST_F(BudgetDatabaseTest, CheckBackgroundBudgetHistogram) { + base::SimpleTestClock* clock = SetClockForTesting(); + + // Set the default site engagement. + SetSiteEngagementScore(kEngagement); + + // Initialize the budget with some interesting chunks: 30 budget (full + // engagement), 15 budget (half of the engagement), 0 budget (less than an + // hour), and then after the first two expire, another 30 budget. + GetBudgetDetails(); + clock->Advance(base::Days(kDefaultExpirationInDays / 2)); + GetBudgetDetails(); + clock->Advance(base::Minutes(59)); + GetBudgetDetails(); + clock->Advance(base::Days(kDefaultExpirationInDays + 1)); + GetBudgetDetails(); + + // The BackgroundBudget UMA is recorded when budget is added to the origin. + // This can happen a maximum of once per hour so there should be two entries. + std::vector<base::Bucket> buckets = + GetHistogramTester()->GetAllSamples("PushMessaging.BackgroundBudget"); + ASSERT_EQ(2U, buckets.size()); + // First bucket is for full award, which should have 2 entries. + double full_award = kMaxDailyBudget * kEngagement / + site_engagement::SiteEngagementScore::kMaxPoints * + kDefaultExpirationInDays; + EXPECT_EQ(floor(full_award), buckets[0].min); + EXPECT_EQ(2, buckets[0].count); + // Second bucket is for 1.5 * award, which should have 1 entry. + EXPECT_EQ(floor(full_award * 1.5), buckets[1].min); + EXPECT_EQ(1, buckets[1].count); +} + +TEST_F(BudgetDatabaseTest, CheckEngagementHistograms) { + base::SimpleTestClock* clock = SetClockForTesting(); + + // Manipulate the engagement so that the budget is twice the cost of an + // action. + double cost = 2; + double engagement = 2 * cost * + site_engagement::SiteEngagementScore::kMaxPoints / + kDefaultExpirationInDays / kMaxDailyBudget; + SetSiteEngagementScore(engagement); + + // Get the budget, which will award a chunk of budget equal to engagement. + GetBudgetDetails(); + + // Now spend the budget to trigger the UMA recording the SES score. The first + // call shouldn't write any UMA. The second should write a lowSES entry, and + // the third should write a noSES entry. + ASSERT_TRUE(SpendBudget(cost)); + ASSERT_TRUE(SpendBudget(cost)); + ASSERT_FALSE(SpendBudget(cost)); + + // Advance the clock by 12 days (to guarantee a full new engagement grant) + // then change the SES score to get a different UMA entry, then spend the + // budget again. + clock->Advance(base::Days(12)); + GetBudgetDetails(); + SetSiteEngagementScore(engagement * 2); + ASSERT_TRUE(SpendBudget(cost)); + ASSERT_TRUE(SpendBudget(cost)); + ASSERT_FALSE(SpendBudget(cost)); + + // Now check the UMA. Both UMA should have 2 buckets with 1 entry each. + std::vector<base::Bucket> no_budget_buckets = + GetHistogramTester()->GetAllSamples("PushMessaging.SESForNoBudgetOrigin"); + ASSERT_EQ(2U, no_budget_buckets.size()); + EXPECT_EQ(floor(engagement), no_budget_buckets[0].min); + EXPECT_EQ(1, no_budget_buckets[0].count); + EXPECT_EQ(floor(engagement * 2), no_budget_buckets[1].min); + EXPECT_EQ(1, no_budget_buckets[1].count); + + std::vector<base::Bucket> low_budget_buckets = + GetHistogramTester()->GetAllSamples( + "PushMessaging.SESForLowBudgetOrigin"); + ASSERT_EQ(2U, low_budget_buckets.size()); + EXPECT_EQ(floor(engagement), low_budget_buckets[0].min); + EXPECT_EQ(1, low_budget_buckets[0].count); + EXPECT_EQ(floor(engagement * 2), low_budget_buckets[1].min); + EXPECT_EQ(1, low_budget_buckets[1].count); +} + +TEST_F(BudgetDatabaseTest, DefaultSiteEngagementInIncognitoProfile) { + TestingProfile second_profile; + Profile* second_profile_incognito = + second_profile.GetPrimaryOTRProfile(/*create_if_needed=*/true); + + // Create a second BudgetDatabase instance for the off-the-record version of + // a second profile. This will not have been influenced by the |profile_|. + std::unique_ptr<BudgetDatabase> second_database = + std::make_unique<BudgetDatabase>(second_profile_incognito); + + ASSERT_FALSE(profile()->IsOffTheRecord()); + ASSERT_FALSE(second_profile.IsOffTheRecord()); + ASSERT_TRUE(second_profile_incognito->IsOffTheRecord()); + + // The Site Engagement Score considered by an Incognito profile must be equal + // to the score considered in a regular profile visting a page for the first + // time. This may grant a small amount of budget, but does mean that Incognito + // mode cannot be detected through the Budget API. + EXPECT_EQ(database()->GetSiteEngagementScoreForOrigin(origin()), + second_database->GetSiteEngagementScoreForOrigin(origin())); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc new file mode 100644 index 00000000000..a09e96fba38 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.cc @@ -0,0 +1,322 @@ +// Copyright 2014 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. + +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" + +#include <string.h> + +#include "base/check_op.h" +#include "base/guid.h" +#include "base/notreached.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/scoped_user_pref_update.h" + +constexpr char kPushMessagingAppIdentifierPrefix[] = "wp:"; +constexpr char kInstanceIDGuidSuffix[] = "-V2"; + +namespace { + +// sizeof is strlen + 1 since it's null-terminated. +constexpr size_t kPrefixLength = sizeof(kPushMessagingAppIdentifierPrefix) - 1; +constexpr size_t kGuidSuffixLength = sizeof(kInstanceIDGuidSuffix) - 1; + +// Ok to use '#' as separator since only the origin of the url is used. +constexpr char kPrefValueSeparator = '#'; +constexpr size_t kGuidLength = 36; // "%08X-%04X-%04X-%04X-%012llX" + +std::string FromTimeToString(base::Time time) { + DCHECK(!time.is_null()); + return base::NumberToString(time.ToDeltaSinceWindowsEpoch().InMilliseconds()); +} + +bool FromStringToTime(const std::string& time_string, + absl::optional<base::Time>* time) { + DCHECK(!time_string.empty()); + int64_t milliseconds; + if (base::StringToInt64(time_string, &milliseconds) && milliseconds > 0) { + *time = absl::make_optional(base::Time::FromDeltaSinceWindowsEpoch( + base::Milliseconds(milliseconds))); + return true; + } + return false; +} + +std::string MakePrefValue( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional<base::Time>& expiration_time = absl::nullopt) { + std::string result = origin.spec() + kPrefValueSeparator + + base::NumberToString(service_worker_registration_id); + if (expiration_time) + result += kPrefValueSeparator + FromTimeToString(*expiration_time); + return result; +} + +bool DisassemblePrefValue(const std::string& pref_value, + GURL* origin, + int64_t* service_worker_registration_id, + absl::optional<base::Time>* expiration_time) { + std::vector<std::string> parts = + base::SplitString(pref_value, std::string(1, kPrefValueSeparator), + base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + + if (parts.size() < 2 || parts.size() > 3) + return false; + + if (!base::StringToInt64(parts[1], service_worker_registration_id)) + return false; + + *origin = GURL(parts[0]); + if (!origin->is_valid()) + return false; + + if (parts.size() == 3) + return FromStringToTime(parts[2], expiration_time); + + return true; +} + +} // namespace + +// static +void PushMessagingAppIdentifier::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + // TODO(johnme): If push becomes enabled in incognito, be careful that this + // pref is read from the right profile, as prefs defined in a regular profile + // are visible in the corresponding incognito profile unless overridden. + // TODO(johnme): Make sure this pref doesn't get out of sync after crashes. + registry->RegisterDictionaryPref(prefs::kPushMessagingAppIdentifierMap); +} + +// static +bool PushMessagingAppIdentifier::UseInstanceID(const std::string& app_id) { + return base::EndsWith(app_id, kInstanceIDGuidSuffix, + base::CompareCase::SENSITIVE); +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::Generate( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional<base::Time>& expiration_time) { + // All new push subscriptions use Instance ID tokens. + return GenerateInternal(origin, service_worker_registration_id, + true /* use_instance_id */, expiration_time); +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::LegacyGenerateForTesting( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional<base::Time>& expiration_time) { + return GenerateInternal(origin, service_worker_registration_id, + false /* use_instance_id */, expiration_time); +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::GenerateInternal( + const GURL& origin, + int64_t service_worker_registration_id, + bool use_instance_id, + const absl::optional<base::Time>& expiration_time) { + // Use uppercase GUID for consistency with GUIDs Push has already sent to GCM. + // Also allows detecting case mangling; see code commented "crbug.com/461867". + std::string guid = base::ToUpperASCII(base::GenerateGUID()); + if (use_instance_id) { + guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength, + kInstanceIDGuidSuffix); + } + CHECK(!guid.empty()); + std::string app_id = kPushMessagingAppIdentifierPrefix + origin.spec() + + kPrefValueSeparator + guid; + + PushMessagingAppIdentifier app_identifier( + app_id, origin, service_worker_registration_id, expiration_time); + app_identifier.DCheckValid(); + return app_identifier; +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::FindByAppId( + Profile* profile, const std::string& app_id) { + if (!base::StartsWith(app_id, kPushMessagingAppIdentifierPrefix, + base::CompareCase::INSENSITIVE_ASCII)) { + return PushMessagingAppIdentifier(); + } + + // Since we now know this is a Push Messaging app_id, check the case hasn't + // been mangled (crbug.com/461867). + DCHECK_EQ(kPushMessagingAppIdentifierPrefix, app_id.substr(0, kPrefixLength)); + DCHECK_GE(app_id.size(), kPrefixLength + kGuidLength); + DCHECK_EQ(app_id.substr(app_id.size() - kGuidLength), + base::ToUpperASCII(app_id.substr(app_id.size() - kGuidLength))); + + const base::Value* map = + profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap); + + const std::string* map_value = map->FindStringKey(app_id); + + if (!map_value || map_value->empty()) + return PushMessagingAppIdentifier(); + + GURL origin; + int64_t service_worker_registration_id; + absl::optional<base::Time> expiration_time; + // Try disassemble the pref value, return an invalid app identifier if the + // pref value is corrupted + if (!DisassemblePrefValue(*map_value, &origin, + &service_worker_registration_id, + &expiration_time)) { + NOTREACHED(); + return PushMessagingAppIdentifier(); + } + + PushMessagingAppIdentifier app_identifier( + app_id, origin, service_worker_registration_id, expiration_time); + app_identifier.DCheckValid(); + return app_identifier; +} + +// static +PushMessagingAppIdentifier PushMessagingAppIdentifier::FindByServiceWorker( + Profile* profile, + const GURL& origin, + int64_t service_worker_registration_id) { + const std::string base_pref_value = + MakePrefValue(origin, service_worker_registration_id); + + const base::Value* map = + profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap); + for (auto entry : map->DictItems()) { + if (entry.second.is_string() && + base::StartsWith(entry.second.GetString(), base_pref_value, + base::CompareCase::SENSITIVE)) { + return FindByAppId(profile, entry.first); + } + } + return PushMessagingAppIdentifier(); +} + +// static +std::vector<PushMessagingAppIdentifier> PushMessagingAppIdentifier::GetAll( + Profile* profile) { + std::vector<PushMessagingAppIdentifier> result; + + const base::Value* map = + profile->GetPrefs()->GetDictionary(prefs::kPushMessagingAppIdentifierMap); + for (auto entry : map->DictItems()) { + result.push_back(FindByAppId(profile, entry.first)); + } + + return result; +} + +// static +void PushMessagingAppIdentifier::DeleteAllFromPrefs(Profile* profile) { + DictionaryPrefUpdate update(profile->GetPrefs(), + prefs::kPushMessagingAppIdentifierMap); + base::Value* map = update.Get(); + map->DictClear(); +} + +// static +size_t PushMessagingAppIdentifier::GetCount(Profile* profile) { + return profile->GetPrefs() + ->GetDictionary(prefs::kPushMessagingAppIdentifierMap) + ->DictSize(); +} + +PushMessagingAppIdentifier::PushMessagingAppIdentifier( + const PushMessagingAppIdentifier& other) = default; + +PushMessagingAppIdentifier::PushMessagingAppIdentifier() + : origin_(GURL::EmptyGURL()), service_worker_registration_id_(-1) {} + +PushMessagingAppIdentifier::PushMessagingAppIdentifier( + const std::string& app_id, + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional<base::Time>& expiration_time) + : app_id_(app_id), + origin_(origin), + service_worker_registration_id_(service_worker_registration_id), + expiration_time_(expiration_time) {} + +PushMessagingAppIdentifier::~PushMessagingAppIdentifier() {} + +bool PushMessagingAppIdentifier::IsExpired() const { + return (expiration_time_) ? *expiration_time_ < base::Time::Now() : false; +} + +void PushMessagingAppIdentifier::PersistToPrefs(Profile* profile) const { + DCheckValid(); + + DictionaryPrefUpdate update(profile->GetPrefs(), + prefs::kPushMessagingAppIdentifierMap); + base::Value* map = update.Get(); + + // Delete any stale entry with the same origin and Service Worker + // registration id (hence we ensure there is a 1:1 not 1:many mapping). + PushMessagingAppIdentifier old = + FindByServiceWorker(profile, origin_, service_worker_registration_id_); + if (!old.is_null()) + map->RemoveKey(old.app_id_); + + map->SetKey(app_id_, + base::Value(MakePrefValue( + origin_, service_worker_registration_id_, expiration_time_))); +} + +void PushMessagingAppIdentifier::DeleteFromPrefs(Profile* profile) const { + DCheckValid(); + + DictionaryPrefUpdate update(profile->GetPrefs(), + prefs::kPushMessagingAppIdentifierMap); + base::Value* map = update.Get(); + map->RemoveKey(app_id_); +} + +void PushMessagingAppIdentifier::DCheckValid() const { +#if DCHECK_IS_ON() + DCHECK_GE(service_worker_registration_id_, 0); + + DCHECK(origin_.is_valid()); + DCHECK_EQ(origin_.DeprecatedGetOriginAsURL(), origin_); + + // "wp:" + DCHECK_EQ(kPushMessagingAppIdentifierPrefix, + app_id_.substr(0, kPrefixLength)); + + // Optional (origin.spec() + '#') + if (app_id_.size() != kPrefixLength + kGuidLength) { + constexpr size_t suffix_length = 1 /* kPrefValueSeparator */ + kGuidLength; + DCHECK_GT(app_id_.size(), kPrefixLength + suffix_length); + DCHECK_EQ(origin_, GURL(app_id_.substr( + kPrefixLength, + app_id_.size() - kPrefixLength - suffix_length))); + DCHECK_EQ(std::string(1, kPrefValueSeparator), + app_id_.substr(app_id_.size() - suffix_length, 1)); + } + + // GUID. In order to distinguish them, an app_id created for an InstanceID + // based subscription has the last few characters of the GUID overwritten with + // kInstanceIDGuidSuffix (which contains non-hex characters invalid in GUIDs). + std::string guid = app_id_.substr(app_id_.size() - kGuidLength); + if (UseInstanceID(app_id_)) { + DCHECK(!base::IsValidGUID(guid)); + + // Replace suffix with valid hex so we can validate the rest of the string. + guid = guid.replace(guid.size() - kGuidSuffixLength, kGuidSuffixLength, + kGuidSuffixLength, 'C'); + } + DCHECK(base::IsValidGUID(guid)); +#endif // DCHECK_IS_ON() +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h new file mode 100644 index 00000000000..631976003fc --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier.h @@ -0,0 +1,152 @@ +// Copyright 2014 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_ + +#include <stddef.h> +#include <stdint.h> +#include <string> +#include <vector> + +#include "base/check.h" +#include "base/gtest_prod_util.h" +#include "base/time/time.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/gurl.h" + +class Profile; + +namespace user_prefs { +class PrefRegistrySyncable; +} + +// The prefix used for all push messaging application ids. +extern const char kPushMessagingAppIdentifierPrefix[]; + +// Type used to identify a Service Worker registration from a Push API +// perspective. These can be persisted to prefs, in a 1:1 mapping between +// app_id (which includes origin) and service_worker_registration_id. +// Legacy mapped values saved by old versions of Chrome are also supported; +// these don't contain the origin in the app_id, so instead they map from +// app_id to pair<origin, service_worker_registration_id>. +class PushMessagingAppIdentifier { + public: + // Register profile-specific prefs. + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Returns whether the modern InstanceID API should be used with this app_id + // (rather than legacy GCM registration). + static bool UseInstanceID(const std::string& app_id); + + // Generates a new app identifier, with partially random app_id. + static PushMessagingAppIdentifier Generate( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional<base::Time>& expiration_time = absl::nullopt); + + // Looks up an app identifier by app_id. If not found, is_null() will be true. + static PushMessagingAppIdentifier FindByAppId(Profile* profile, + const std::string& app_id); + + // Looks up an app identifier by origin & service worker registration id. + // If not found, is_null() will be true. + static PushMessagingAppIdentifier FindByServiceWorker( + Profile* profile, + const GURL& origin, + int64_t service_worker_registration_id); + + // Returns all the PushMessagingAppIdentifiers currently registered for the + // given |profile|. + static std::vector<PushMessagingAppIdentifier> GetAll(Profile* profile); + + // Deletes all PushMessagingAppIdentifiers currently registered for the given + // |profile|. + static void DeleteAllFromPrefs(Profile* profile); + + // Returns the number of PushMessagingAppIdentifiers currently registered for + // the given |profile|. + static size_t GetCount(Profile* profile); + + ~PushMessagingAppIdentifier(); + + // Persist this app identifier to prefs. + void PersistToPrefs(Profile* profile) const; + + // Delete this app identifier from prefs. + void DeleteFromPrefs(Profile* profile) const; + + // Returns true if this identifier does not represent an app (i.e. this was + // returned by a failed Find call). + bool is_null() const { return service_worker_registration_id_ < 0; } + + // String that should be passed to push services like GCM to identify a + // particular Service Worker (so we can route incoming messages). Example: + // wp:https://foo.example.com:8443/#9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F + // Legacy app_ids have no origin, e.g. wp:9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F + const std::string& app_id() const { + DCHECK(!is_null()); + return app_id_; + } + + const GURL& origin() const { + DCHECK(!is_null()); + return origin_; + } + + int64_t service_worker_registration_id() const { + DCHECK(!is_null()); + return service_worker_registration_id_; + } + + void set_expiration_time(const absl::optional<base::Time>& expiration_time) { + expiration_time_ = expiration_time; + } + + bool IsExpired() const; + + absl::optional<base::Time> expiration_time() const { + DCHECK(!is_null()); + return expiration_time_; + } + + // Copy constructor + PushMessagingAppIdentifier(const PushMessagingAppIdentifier& other); + + private: + friend class PushMessagingAppIdentifierTest; + friend class PushMessagingBrowserTestBase; + FRIEND_TEST_ALL_PREFIXES(PushMessagingAppIdentifierTest, FindLegacy); + + // Generates a new app identifier for legacy GCM (not modern InstanceID). + static PushMessagingAppIdentifier LegacyGenerateForTesting( + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional<base::Time>& expiration_time = absl::nullopt); + + static PushMessagingAppIdentifier GenerateInternal( + const GURL& origin, + int64_t service_worker_registration_id, + bool use_instance_id, + const absl::optional<base::Time>& expiration_time = absl::nullopt); + + // Constructs an invalid app identifier. + PushMessagingAppIdentifier(); + // Constructs a valid app identifier. + PushMessagingAppIdentifier( + const std::string& app_id, + const GURL& origin, + int64_t service_worker_registration_id, + const absl::optional<base::Time>& expiration_time = absl::nullopt); + + // Validates that all the fields contain valid values. + void DCheckValid() const; + + std::string app_id_; + GURL origin_; + int64_t service_worker_registration_id_; + absl::optional<base::Time> expiration_time_; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_APP_IDENTIFIER_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc new file mode 100644 index 00000000000..5b7c649c2e4 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_app_identifier_unittest.cc @@ -0,0 +1,310 @@ +// Copyright 2014 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. + +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" + +#include <stdint.h> + +#include "base/time/time.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +void ExpectAppIdentifiersEqual(const PushMessagingAppIdentifier& a, + const PushMessagingAppIdentifier& b) { + EXPECT_EQ(a.app_id(), b.app_id()); + EXPECT_EQ(a.origin(), b.origin()); + EXPECT_EQ(a.service_worker_registration_id(), + b.service_worker_registration_id()); + EXPECT_EQ(a.expiration_time(), b.expiration_time()); +} + +base::Time kExpirationTime = + base::Time::FromDeltaSinceWindowsEpoch(base::Seconds(1)); + +} // namespace + +class PushMessagingAppIdentifierTest : public testing::Test { + protected: + PushMessagingAppIdentifier GenerateId( + const GURL& origin, + int64_t service_worker_registration_id) { + // To bypass DCHECK in PushMessagingAppIdentifier::Generate, we just use it + // to generate app_id, and then use private constructor. + std::string app_id = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 1) + .app_id(); + return PushMessagingAppIdentifier(app_id, origin, + service_worker_registration_id); + } + + void SetUp() override { + original_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 1); + same_origin_and_sw_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com"), 1); + different_origin_ = PushMessagingAppIdentifier::Generate( + GURL("https://foobar.example.com/"), 1); + different_sw_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 42); + with_et_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 1, kExpirationTime); + different_et_ = PushMessagingAppIdentifier::Generate( + GURL("https://www.example.com/"), 1, + kExpirationTime + base::Seconds(100)); + } + + Profile* profile() { return &profile_; } + + PushMessagingAppIdentifier original_; + PushMessagingAppIdentifier same_origin_and_sw_; + PushMessagingAppIdentifier different_origin_; + PushMessagingAppIdentifier different_sw_; + PushMessagingAppIdentifier different_et_; + PushMessagingAppIdentifier with_et_; + + private: + content::BrowserTaskEnvironment task_environment_; + TestingProfile profile_; +}; + +TEST_F(PushMessagingAppIdentifierTest, ConstructorValidity) { + // The following two are valid: + EXPECT_FALSE(GenerateId(GURL("https://www.example.com/"), 1).is_null()); + EXPECT_FALSE(GenerateId(GURL("https://www.example.com"), 1).is_null()); + // The following four are invalid and will DCHECK in Generate: + EXPECT_FALSE(GenerateId(GURL(""), 1).is_null()); + EXPECT_FALSE(GenerateId(GURL("foo"), 1).is_null()); + EXPECT_FALSE(GenerateId(GURL("https://www.example.com/foo"), 1).is_null()); + EXPECT_FALSE(GenerateId(GURL("https://www.example.com/#foo"), 1).is_null()); + // The following one is invalid and will DCHECK in Generate and be null: + EXPECT_TRUE(GenerateId(GURL("https://www.example.com/"), -1).is_null()); +} + +TEST_F(PushMessagingAppIdentifierTest, UniqueGuids) { + EXPECT_NE( + PushMessagingAppIdentifier::Generate(GURL("https://www.example.com/"), 1) + .app_id(), + PushMessagingAppIdentifier::Generate(GURL("https://www.example.com/"), 1) + .app_id()); +} + +TEST_F(PushMessagingAppIdentifierTest, FindInvalidAppId) { + // These calls to FindByAppId should not DCHECK. + EXPECT_TRUE(PushMessagingAppIdentifier::FindByAppId(profile(), "").is_null()); + EXPECT_TRUE(PushMessagingAppIdentifier::FindByAppId( + profile(), "amhfneadkjmnlefnpidcijoldiibcdnd") + .is_null()); +} + +TEST_F(PushMessagingAppIdentifierTest, PersistAndFind) { + ASSERT_TRUE( + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()) + .is_null()); + + const auto identifier = PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + + ASSERT_TRUE(identifier.is_null()); + + // Test basic PersistToPrefs round trips. + original_.PersistToPrefs(profile()); + { + PushMessagingAppIdentifier found_by_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_FALSE(found_by_app_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_app_id); + } + { + PushMessagingAppIdentifier found_by_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_FALSE(found_by_origin_and_swr_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_origin_and_swr_id); + } +} + +TEST_F(PushMessagingAppIdentifierTest, FindLegacy) { + const std::string legacy_app_id("wp:9CC55CCE-B8F9-4092-A364-3B0F73A3AB5F"); + ASSERT_TRUE(PushMessagingAppIdentifier::FindByAppId(profile(), legacy_app_id) + .is_null()); + + const auto identifier = PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + + ASSERT_TRUE(identifier.is_null()); + + // Create a legacy preferences entry (the test happens to use PersistToPrefs + // since that currently works, but it's ok to change the behavior of + // PersistToPrefs; if so, this test can just do a raw DictionaryPrefUpdate). + original_.app_id_ = legacy_app_id; + original_.PersistToPrefs(profile()); + + // Test that legacy entries can be read back from prefs. + { + PushMessagingAppIdentifier found_by_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_FALSE(found_by_app_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_app_id); + } + { + PushMessagingAppIdentifier found_by_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_FALSE(found_by_origin_and_swr_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_origin_and_swr_id); + } +} + +TEST_F(PushMessagingAppIdentifierTest, PersistOverwritesSameOriginAndSW) { + original_.PersistToPrefs(profile()); + + // Test that PersistToPrefs overwrites when same origin and Service Worker. + ASSERT_NE(original_.app_id(), same_origin_and_sw_.app_id()); + ASSERT_EQ(original_.origin(), same_origin_and_sw_.origin()); + ASSERT_EQ(original_.service_worker_registration_id(), + same_origin_and_sw_.service_worker_registration_id()); + same_origin_and_sw_.PersistToPrefs(profile()); + { + PushMessagingAppIdentifier found_by_original_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_TRUE(found_by_original_app_id.is_null()); + } + { + PushMessagingAppIdentifier found_by_soas_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), + same_origin_and_sw_.app_id()); + EXPECT_FALSE(found_by_soas_app_id.is_null()); + ExpectAppIdentifiersEqual(same_origin_and_sw_, found_by_soas_app_id); + } + { + PushMessagingAppIdentifier found_by_original_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_FALSE(found_by_original_origin_and_swr_id.is_null()); + ExpectAppIdentifiersEqual(same_origin_and_sw_, + found_by_original_origin_and_swr_id); + } +} + +TEST_F(PushMessagingAppIdentifierTest, PersistDoesNotOverwriteDifferent) { + original_.PersistToPrefs(profile()); + + // Test that PersistToPrefs doesn't overwrite when different origin or SW. + ASSERT_NE(original_.app_id(), different_origin_.app_id()); + ASSERT_NE(original_.app_id(), different_sw_.app_id()); + different_origin_.PersistToPrefs(profile()); + different_sw_.PersistToPrefs(profile()); + { + PushMessagingAppIdentifier found_by_original_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_FALSE(found_by_original_app_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_original_app_id); + } + { + PushMessagingAppIdentifier found_by_original_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_FALSE(found_by_original_origin_and_swr_id.is_null()); + ExpectAppIdentifiersEqual(original_, found_by_original_origin_and_swr_id); + } +} + +TEST_F(PushMessagingAppIdentifierTest, DeleteFromPrefs) { + original_.PersistToPrefs(profile()); + different_origin_.PersistToPrefs(profile()); + different_sw_.PersistToPrefs(profile()); + + // Test DeleteFromPrefs. Deleted app identifier should be deleted. + original_.DeleteFromPrefs(profile()); + { + PushMessagingAppIdentifier found_by_original_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), original_.app_id()); + EXPECT_TRUE(found_by_original_app_id.is_null()); + } + { + PushMessagingAppIdentifier found_by_original_origin_and_swr_id = + PushMessagingAppIdentifier::FindByServiceWorker( + profile(), original_.origin(), + original_.service_worker_registration_id()); + EXPECT_TRUE(found_by_original_origin_and_swr_id.is_null()); + } +} + +TEST_F(PushMessagingAppIdentifierTest, GetAll) { + original_.PersistToPrefs(profile()); + different_origin_.PersistToPrefs(profile()); + different_sw_.PersistToPrefs(profile()); + + original_.DeleteFromPrefs(profile()); + + // Test GetAll. Non-deleted app identifiers should all be listed. + std::vector<PushMessagingAppIdentifier> all_app_identifiers = + PushMessagingAppIdentifier::GetAll(profile()); + EXPECT_EQ(2u, all_app_identifiers.size()); + // Order is unspecified. + bool contained_different_origin = false; + bool contained_different_sw = false; + for (const PushMessagingAppIdentifier& app_identifier : all_app_identifiers) { + if (app_identifier.app_id() == different_origin_.app_id()) { + ExpectAppIdentifiersEqual(different_origin_, app_identifier); + contained_different_origin = true; + } else { + ExpectAppIdentifiersEqual(different_sw_, app_identifier); + contained_different_sw = true; + } + } + EXPECT_TRUE(contained_different_origin); + EXPECT_TRUE(contained_different_sw); +} + +TEST_F(PushMessagingAppIdentifierTest, PersistWithExpirationTime) { + ASSERT_TRUE(with_et_.expiration_time()); + ASSERT_TRUE(different_et_.expiration_time()); + ASSERT_EQ(with_et_.origin(), different_et_.origin()); + ASSERT_EQ(with_et_.service_worker_registration_id(), + different_et_.service_worker_registration_id()); + ASSERT_FALSE(kExpirationTime.is_null()); + + different_et_.PersistToPrefs(profile()); + + // Test PersistToPrefs and FindByAppId, whether expiration time is saved + // properly + std::vector<PushMessagingAppIdentifier> all_app_identifiers = + PushMessagingAppIdentifier::GetAll(profile()); + EXPECT_EQ(1u, all_app_identifiers.size()); + { + PushMessagingAppIdentifier found_by_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), + different_et_.app_id()); + // Check whether expiration time was saved + ExpectAppIdentifiersEqual(found_by_app_id, different_et_); + } + with_et_.PersistToPrefs(profile()); + { + all_app_identifiers = PushMessagingAppIdentifier::GetAll(profile()); + EXPECT_EQ(1u, all_app_identifiers.size()); + } + { + PushMessagingAppIdentifier found_by_with_et_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), with_et_.app_id()); + EXPECT_FALSE(found_by_with_et_app_id.is_null()); + EXPECT_EQ(found_by_with_et_app_id.expiration_time(), kExpirationTime); + ExpectAppIdentifiersEqual(found_by_with_et_app_id, with_et_); + } + { + PushMessagingAppIdentifier found_by_different_et_app_id = + PushMessagingAppIdentifier::FindByAppId(profile(), + different_et_.app_id()); + EXPECT_TRUE(found_by_different_et_app_id.is_null()); + } +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc b/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc new file mode 100644 index 00000000000..b94212aad9f --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_browsertest.cc @@ -0,0 +1,3227 @@ +// Copyright 2014 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. + +#include <stddef.h> +#include <stdint.h> + +#include <map> +#include <memory> +#include <string> + +#include "base/barrier_closure.h" +#include "base/base64url.h" +#include "base/bind.h" +#include "base/command_line.h" +#include "base/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/bind.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" +#include "chrome/browser/notifications/notification_display_service_tester.h" +#include "chrome/browser/notifications/notification_handler.h" +#include "chrome/browser/permissions/crowd_deny_fake_safe_browsing_database_manager.h" +#include "chrome/browser/permissions/crowd_deny_preload_data.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "chrome/browser/push_messaging/push_messaging_features.h" +#include "chrome/browser/push_messaging/push_messaging_service_factory.h" +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" +#include "chrome/browser/safe_browsing/test_safe_browsing_service.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/buildflags.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/browsing_data/content/browsing_data_helper.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/content_settings/core/common/content_settings_types.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/fake_gcm_profile_service.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/instance_id/fake_gcm_driver_for_instance_id.h" +#include "components/gcm_driver/instance_id/instance_id_driver.h" +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" +#include "components/keep_alive_registry/keep_alive_registry.h" +#include "components/keep_alive_registry/keep_alive_types.h" +#include "components/network_session_configurator/common/network_switches.h" +#include "components/permissions/permission_request_manager.h" +#include "components/site_engagement/content/site_engagement_score.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "content/public/browser/browsing_data_remover.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_features.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/browsing_data_remover_test_util.h" +#include "content/public/test/prerender_test_util.h" +#include "content/public/test/test_utils.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" +#include "ui/base/window_open_disposition.h" +#include "ui/message_center/public/cpp/notification.h" + +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) +#include "chrome/browser/background/background_mode_manager.h" +#endif + +namespace { + +const char kManifestSenderId[] = "1234567890"; +const int32_t kApplicationServerKeyLength = 65; + +enum class PushSubscriptionKeyFormat { kOmitKey, kBinary, kBase64UrlEncoded }; + +// NIST P-256 public key made available to tests. Must be an uncompressed +// point in accordance with SEC1 2.3.3. +const uint8_t kApplicationServerKey[kApplicationServerKeyLength] = { + 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36, + 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48, + 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B, + 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13, + 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76, + 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD}; + +// URL-safe base64 encoded version of the |kApplicationServerKey|. +const char kEncodedApplicationServerKey[] = + "BFVSaqVujqpHlzYQwWY8HmW_oXvuSMnGu78CGFNyHQx7qeMRtwNSIdNxkBOowc_tIPcf0X_ydr" + "YBINg1pdk8Q_0"; + +// From chrome/browser/push_messaging/push_messaging_manager.cc +const char* kIncognitoWarningPattern = + "Chrome currently does not support the Push API in incognito mode " + "(https://crbug.com/401439). There is deliberately no way to " + "feature-detect this, since incognito mode needs to be undetectable by " + "websites."; + +std::string GetTestApplicationServerKey(bool base64_url_encoded = false) { + std::string application_server_key; + + if (base64_url_encoded) { + base::Base64UrlEncode(reinterpret_cast<const char*>(kApplicationServerKey), + base::Base64UrlEncodePolicy::OMIT_PADDING, + &application_server_key); + } else { + application_server_key = + std::string(kApplicationServerKey, + kApplicationServerKey + base::size(kApplicationServerKey)); + } + + return application_server_key; +} + +void LegacyRegisterCallback(base::OnceClosure done_callback, + std::string* out_registration_id, + gcm::GCMClient::Result* out_result, + const std::string& registration_id, + gcm::GCMClient::Result result) { + if (out_registration_id) + *out_registration_id = registration_id; + if (out_result) + *out_result = result; + std::move(done_callback).Run(); +} + +void DidRegister(base::OnceClosure done_callback, + const std::string& registration_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth, + blink::mojom::PushRegistrationStatus status) { + EXPECT_EQ(blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE, + status); + std::move(done_callback).Run(); +} + +void InstanceIDResultCallback(base::OnceClosure done_callback, + instance_id::InstanceID::Result* out_result, + instance_id::InstanceID::Result result) { + DCHECK(out_result); + *out_result = result; + std::move(done_callback).Run(); +} + +} // namespace + +class PushMessagingBrowserTestBase : public InProcessBrowserTest { + public: + PushMessagingBrowserTestBase() + : scoped_testing_factory_installer_( + base::BindRepeating(&gcm::FakeGCMProfileService::Build)), + gcm_service_(nullptr), + gcm_driver_(nullptr) {} + + ~PushMessagingBrowserTestBase() override = default; + + PushMessagingBrowserTestBase(const PushMessagingBrowserTestBase&) = delete; + PushMessagingBrowserTestBase& operator=(const PushMessagingBrowserTestBase&) = + delete; + + // InProcessBrowserTest: + void SetUp() override { + https_server_ = std::make_unique<net::EmbeddedTestServer>( + net::EmbeddedTestServer::TYPE_HTTPS); + https_server_->ServeFilesFromSourceDirectory(GetChromeTestDataDir()); + content::SetupCrossSiteRedirector(https_server_.get()); + + site_engagement::SiteEngagementScore::SetParamValuesForTesting(); + InProcessBrowserTest::SetUp(); + } + void SetUpCommandLine(base::CommandLine* command_line) override { + // Enable experimental features for subscription restrictions. + command_line->AppendSwitch( + switches::kEnableExperimentalWebPlatformFeatures); + + // HTTPS server only serves a valid cert for localhost, so this is needed to + // load webby domains like "embedded.com" without an interstitial. + command_line->AppendSwitch(switches::kIgnoreCertificateErrors); + } + + // InProcessBrowserTest: + void SetUpOnMainThread() override { + host_resolver()->AddRule("*", "127.0.0.1"); + ASSERT_TRUE(https_server_->Start()); + + KeyedService* keyed_service = + gcm::GCMProfileServiceFactory::GetForProfile(GetBrowser()->profile()); + if (keyed_service) { + gcm_service_ = static_cast<gcm::FakeGCMProfileService*>(keyed_service); + gcm_driver_ = static_cast<instance_id::FakeGCMDriverForInstanceID*>( + gcm_service_->driver()); + } + + notification_tester_ = std::make_unique<NotificationDisplayServiceTester>( + GetBrowser()->profile()); + + push_service_ = + PushMessagingServiceFactory::GetForProfile(GetBrowser()->profile()); + + LoadTestPage(); + } + + void TearDownOnMainThread() override { + notification_tester_.reset(); + InProcessBrowserTest::TearDownOnMainThread(); + } + + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void RestartPushService() { + Profile* profile = GetBrowser()->profile(); + PushMessagingServiceFactory::GetInstance()->SetTestingFactory( + profile, BrowserContextKeyedServiceFactory::TestingFactory()); + ASSERT_EQ(nullptr, PushMessagingServiceFactory::GetForProfile(profile)); + PushMessagingServiceFactory::GetInstance()->RestoreFactoryForTests(profile); + PushMessagingServiceImpl::InitializeForProfile(profile); + push_service_ = PushMessagingServiceFactory::GetForProfile(profile); + } + + // Helper function to test if a Keep Alive is registered while avoiding the + // platform checks. Returns a boolean so that assertion failures are reported + // at the right line. + // Returns true when KeepAlives are not supported by the platform, or when + // the registration state is equal to the expectation. + bool IsRegisteredKeepAliveEqualTo(bool expectation) { +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + return expectation == + KeepAliveRegistry::GetInstance()->IsOriginRegistered( + KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE); +#else + return true; +#endif + } + + void LoadTestPage(const std::string& path) { + ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), + https_server_->GetURL(path))); + } + + void LoadTestPage() { LoadTestPage(GetTestURL()); } + + void LoadTestPageWithoutManifest() { LoadTestPage(GetNoManifestTestURL()); } + + bool RunScript(const std::string& script, std::string* result) { + return RunScript(script, result, nullptr); + } + + bool RunScript(const std::string& script, std::string* result, + content::WebContents* web_contents) { + if (!web_contents) + web_contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); + return content::ExecuteScriptAndExtractString(web_contents->GetMainFrame(), + script, result); + } + + gcm::GCMAppHandler* GetAppHandler() { + return gcm_driver_->GetAppHandler(kPushMessagingAppIdentifierPrefix); + } + + permissions::PermissionRequestManager* GetPermissionRequestManager() { + return permissions::PermissionRequestManager::FromWebContents( + GetBrowser()->tab_strip_model()->GetActiveWebContents()); + } + + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void RequestAndAcceptPermission(); + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void RequestAndDenyPermission(); + + // Sets out_token to the subscription token (not including server URL). + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void SubscribeSuccessfully( + PushSubscriptionKeyFormat key_format = PushSubscriptionKeyFormat::kBinary, + std::string* out_token = nullptr); + + // Sets up the state corresponding to a dangling push subscription whose + // service worker registration no longer exists. Some users may be left with + // such orphaned subscriptions due to service worker unregistrations not + // clearing push subscriptions in the past. This allows us to emulate that. + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void SetupOrphanedPushSubscription(std::string* out_app_id); + + // Legacy subscribe path using GCMDriver rather than Instance IDs. Only + // for testing that we maintain support for existing stored registrations. + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void LegacySubscribeSuccessfully(std::string* out_subscription_id = nullptr); + + // Strips server URL from a registration endpoint to get subscription token. + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void EndpointToToken(const std::string& endpoint, + bool standard_protocol = true, + std::string* out_token = nullptr); + + blink::mojom::PushSubscriptionPtr GetSubscriptionForAppIdentifier( + const PushMessagingAppIdentifier& app_identifier) { + blink::mojom::PushSubscriptionPtr result; + base::RunLoop run_loop; + push_service_->GetPushSubscriptionFromAppIdentifier( + app_identifier, + base::BindLambdaForTesting( + [&](blink::mojom::PushSubscriptionPtr subscription) { + result = std::move(subscription); + run_loop.Quit(); + })); + run_loop.Run(); + return result; + } + + // Deletes an Instance ID from the GCM Store but keeps the push subscription + // stored in the PushMessagingAppIdentifier map and Service Worker DB. + // Calls should be wrapped in the ASSERT_NO_FATAL_FAILURE() macro. + void DeleteInstanceIDAsIfGCMStoreReset(const std::string& app_id); + + PushMessagingAppIdentifier GetAppIdentifierForServiceWorkerRegistration( + int64_t service_worker_registration_id); + + void SendMessageAndWaitUntilHandled( + const PushMessagingAppIdentifier& app_identifier, + const gcm::IncomingMessage& message); + + net::EmbeddedTestServer* https_server() const { return https_server_.get(); } + + // Returns a vector of the currently displayed Notification objects. + std::vector<message_center::Notification> GetDisplayedNotifications() { + return notification_tester_->GetDisplayedNotificationsForType( + NotificationHandler::Type::WEB_PERSISTENT); + } + + // Returns the number of notifications that are currently being shown. + size_t GetNotificationCount() { return GetDisplayedNotifications().size(); } + + // Removes all shown notifications. + void RemoveAllNotifications() { + notification_tester_->RemoveAllNotifications( + NotificationHandler::Type::WEB_PERSISTENT, true /* by_user */); + } + + // To be called when delivery of a push message has finished. The |run_loop| + // will be told to quit after |messages_required| messages were received. + void OnDeliveryFinished(std::vector<size_t>* number_of_notifications_shown, + base::OnceClosure done_closure) { + DCHECK(number_of_notifications_shown); + number_of_notifications_shown->push_back(GetNotificationCount()); + + std::move(done_closure).Run(); + } + + PushMessagingServiceImpl* push_service() const { return push_service_; } + + void SetSiteEngagementScore(const GURL& url, double score) { + site_engagement::SiteEngagementService* service = + site_engagement::SiteEngagementService::Get(GetBrowser()->profile()); + service->ResetBaseScoreForURL(url, score); + EXPECT_EQ(score, service->GetScore(url)); + } + + // Matches |tag| against the notification's ID to see if the notification's + // js-provided tag could have been |tag|. This is not perfect as it might + // return true for a |tag| that is a substring of the original tag. + static bool TagEquals(const message_center::Notification& notification, + const std::string& tag) { + return std::string::npos != notification.id().find(tag); + } + + protected: + virtual std::string GetTestURL() { return "/push_messaging/test.html"; } + + virtual std::string GetNoManifestTestURL() { + return "/push_messaging/test_no_manifest.html"; + } + + virtual Browser* GetBrowser() const { return browser(); } + + gcm::GCMProfileServiceFactory::ScopedTestingFactoryInstaller + scoped_testing_factory_installer_; + + raw_ptr<gcm::FakeGCMProfileService> gcm_service_; + raw_ptr<instance_id::FakeGCMDriverForInstanceID> gcm_driver_; + base::HistogramTester histogram_tester_; + + std::unique_ptr<NotificationDisplayServiceTester> notification_tester_; + + private: + std::unique_ptr<net::EmbeddedTestServer> https_server_; + raw_ptr<PushMessagingServiceImpl> push_service_; +}; + +void PushMessagingBrowserTestBase::RequestAndAcceptPermission() { + std::string script_result; + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::ACCEPT_ALL); + ASSERT_TRUE(RunScript("requestNotificationPermission();", &script_result)); + ASSERT_EQ("permission status - granted", script_result); +} + +void PushMessagingBrowserTestBase::RequestAndDenyPermission() { + std::string script_result; + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::DENY_ALL); + ASSERT_TRUE(RunScript("requestNotificationPermission();", &script_result)); + ASSERT_EQ("permission status - denied", script_result); +} + +void PushMessagingBrowserTestBase::SubscribeSuccessfully( + PushSubscriptionKeyFormat key_format, + std::string* out_token) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + switch (key_format) { + case PushSubscriptionKeyFormat::kBinary: + ASSERT_TRUE(RunScript("removeManifest()", &script_result)); + ASSERT_EQ("manifest removed", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, true, out_token)); + break; + case PushSubscriptionKeyFormat::kBase64UrlEncoded: + ASSERT_TRUE(RunScript("removeManifest()", &script_result)); + ASSERT_EQ("manifest removed", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePushWithBase64URLEncodedString()", + &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, true, out_token)); + break; + case PushSubscriptionKeyFormat::kOmitKey: + // Test backwards compatibility with old ID based subscriptions. + ASSERT_TRUE( + RunScript("documentSubscribePushWithoutKey()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result, false, out_token)); + break; + default: + NOTREACHED(); + } +} + +void PushMessagingBrowserTestBase::SetupOrphanedPushSubscription( + std::string* out_app_id) { + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + GURL requesting_origin = + https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + // Use 1234LL as it's unlikely to collide with an active service worker + // registration id (they increment from 0). + const int64_t service_worker_registration_id = 1234LL; + + auto options = blink::mojom::PushSubscriptionOptions::New(); + options->user_visible_only = true; + + std::string test_application_server_key = GetTestApplicationServerKey(); + options->application_server_key = std::vector<uint8_t>( + test_application_server_key.begin(), test_application_server_key.end()); + + base::RunLoop run_loop; + push_service()->SubscribeFromWorker( + requesting_origin, service_worker_registration_id, std::move(options), + base::BindOnce(&DidRegister, run_loop.QuitClosure())); + run_loop.Run(); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), requesting_origin, + service_worker_registration_id); + ASSERT_FALSE(app_identifier.is_null()); + *out_app_id = app_identifier.app_id(); +} + +void PushMessagingBrowserTestBase::LegacySubscribeSuccessfully( + std::string* out_subscription_id) { + // Create a non-InstanceID GCM registration. Have to directly access + // GCMDriver, since this codepath has been deleted from Push. + + std::string script_result; + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + GURL requesting_origin = + https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + int64_t service_worker_registration_id = 0LL; + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::LegacyGenerateForTesting( + requesting_origin, service_worker_registration_id); + push_service_->IncreasePushSubscriptionCount(1, true /* is_pending */); + + std::string subscription_id; + { + base::RunLoop run_loop; + gcm::GCMClient::Result register_result = gcm::GCMClient::UNKNOWN_ERROR; + gcm_driver_->Register( + app_identifier.app_id(), {kManifestSenderId}, + base::BindOnce(&LegacyRegisterCallback, run_loop.QuitClosure(), + &subscription_id, ®ister_result)); + run_loop.Run(); + ASSERT_EQ(gcm::GCMClient::SUCCESS, register_result); + } + + app_identifier.PersistToPrefs(GetBrowser()->profile()); + push_service_->IncreasePushSubscriptionCount(1, false /* is_pending */); + push_service_->DecreasePushSubscriptionCount(1, true /* was_pending */); + + { + base::RunLoop run_loop; + push_service_->StorePushSubscriptionForTesting( + GetBrowser()->profile(), requesting_origin, + service_worker_registration_id, subscription_id, kManifestSenderId, + run_loop.QuitClosure()); + run_loop.Run(); + } + + if (out_subscription_id) + *out_subscription_id = subscription_id; +} + +void PushMessagingBrowserTestBase::EndpointToToken(const std::string& endpoint, + bool standard_protocol, + std::string* out_token) { + size_t last_slash = endpoint.rfind('/'); + + ASSERT_EQ(kPushMessagingGcmEndpoint, endpoint.substr(0, last_slash + 1)); + + ASSERT_LT(last_slash + 1, endpoint.length()); // Token must not be empty. + + if (out_token) + *out_token = endpoint.substr(last_slash + 1); +} + +PushMessagingAppIdentifier +PushMessagingBrowserTestBase::GetAppIdentifierForServiceWorkerRegistration( + int64_t service_worker_registration_id) { + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, service_worker_registration_id); + EXPECT_FALSE(app_identifier.is_null()); + return app_identifier; +} + +void PushMessagingBrowserTestBase::DeleteInstanceIDAsIfGCMStoreReset( + const std::string& app_id) { + // Delete the Instance ID directly, keeping the push subscription stored in + // the PushMessagingAppIdentifier map and the Service Worker database. This + // simulates the GCM Store getting reset but failing to clear push + // subscriptions, either because the store got reset before + // 93ec793ac69a542b2213297737178a55d069fd0d (Chrome 56), or because a race + // condition (e.g. shutdown) prevents PushMessagingServiceImpl::OnStoreReset + // from clearing all subscriptions. + instance_id::InstanceIDProfileService* instance_id_profile_service = + instance_id::InstanceIDProfileServiceFactory::GetForProfile( + GetBrowser()->profile()); + DCHECK(instance_id_profile_service); + instance_id::InstanceIDDriver* instance_id_driver = + instance_id_profile_service->driver(); + DCHECK(instance_id_driver); + instance_id::InstanceID::Result delete_result = + instance_id::InstanceID::UNKNOWN_ERROR; + base::RunLoop run_loop; + instance_id_driver->GetInstanceID(app_id)->DeleteID(base::BindOnce( + &InstanceIDResultCallback, run_loop.QuitClosure(), &delete_result)); + run_loop.Run(); + ASSERT_EQ(instance_id::InstanceID::SUCCESS, delete_result); +} + +void PushMessagingBrowserTestBase::SendMessageAndWaitUntilHandled( + const PushMessagingAppIdentifier& app_identifier, + const gcm::IncomingMessage& message) { + base::RunLoop run_loop; + push_service()->SetMessageCallbackForTesting(run_loop.QuitClosure()); + push_service()->OnMessage(app_identifier.app_id(), message); + run_loop.Run(); +} + +class PushMessagingBrowserTest : public PushMessagingBrowserTestBase { + public: + PushMessagingBrowserTest() { + feature_list_.InitAndDisableFeature( + features::kPushMessagingDisallowSenderIDs); + } + + private: + base::test::ScopedFeatureList feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeWithoutKeySuccessNotificationsGranted) { + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey)); + EXPECT_EQ(kManifestSenderId, gcm_driver_->last_gettoken_authorized_entity()); + EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(), + gcm_driver_->last_gettoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeSuccessNotificationsGranted) { + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + EXPECT_EQ(kEncodedApplicationServerKey, + gcm_driver_->last_gettoken_authorized_entity()); + EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(), + gcm_driver_->last_gettoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeSuccessNotificationsGrantedWithBase64URLKey) { + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBase64UrlEncoded)); + EXPECT_EQ(kEncodedApplicationServerKey, + gcm_driver_->last_gettoken_authorized_entity()); + EXPECT_EQ(GetAppIdentifierForServiceWorkerRegistration(0LL).app_id(), + gcm_driver_->last_gettoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeSuccessNotificationsPrompt) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::ACCEPT_ALL); + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + // Both of these methods EXPECT that they succeed. + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result)); + GetAppIdentifierForServiceWorkerRegistration(0LL); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeFailureNotificationsBlocked) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndDenyPermission()); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + EXPECT_EQ("NotAllowedError - Registration failed - permission denied", + script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeFailureNoManifest) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + ASSERT_TRUE(RunScript("removeManifest()", &script_result)); + ASSERT_EQ("manifest removed", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "manifest empty or missing", + script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeFailureNoSenderId) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + ASSERT_TRUE(RunScript("swapManifestNoSenderId()", &script_result)); + ASSERT_EQ("sender id removed from manifest", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "gcm_sender_id not found in manifest", + script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + RegisterFailureEmptyPushSubscriptionOptions) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + ASSERT_TRUE( + RunScript("documentSubscribePushWithEmptyOptions()", &script_result)); + EXPECT_EQ("NotAllowedError - Registration failed - permission denied", + script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeWithInvalidation) { + std::string token1, token2, token3; + + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token1)); + ASSERT_FALSE(token1.empty()); + + // Repeated calls to |subscribe()| should yield the same token. + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2)); + ASSERT_EQ(token1, token2); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), + https_server()->GetURL("/").DeprecatedGetOriginAsURL(), + 0LL /* service_worker_registration_id */); + + ASSERT_FALSE(app_identifier.is_null()); + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + // Delete the InstanceID. This captures two scenarios: either the database was + // corrupted, or the subscription was invalidated by the server. + ASSERT_NO_FATAL_FAILURE( + DeleteInstanceIDAsIfGCMStoreReset(app_identifier.app_id())); + + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id()); + + // Repeated calls to |subscribe()| will now (silently) result in a new token. + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token3)); + ASSERT_FALSE(token3.empty()); + EXPECT_NE(token1, token3); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribeWorker) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Try to subscribe from a worker without a key. This should fail. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "gcm_sender_id not found in manifest", + script_result); + + // Now run the subscribe with a key. This should succeed. + ASSERT_TRUE(RunScript("workerSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, true /* standard_protocol */)); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + SubscribeWorkerWithBase64URLEncodedString) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Try to subscribe from a worker without a key. This should fail. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "gcm_sender_id not found in manifest", + script_result); + + // Now run the subscribe with a key. This should succeed. + ASSERT_TRUE(RunScript("workerSubscribePushWithBase64URLEncodedString()", + &script_result)); + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, true /* standard_protocol */)); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingWithKeyInManifest) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscription from the document without a key, this will trigger + // the code to read sender id from the manifest and will write it to the + // datastore. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + std::string token1; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token1)); + + ASSERT_TRUE(RunScript("removeManifest()", &script_result)); + ASSERT_EQ("manifest removed", script_result); + + // Try to resubscribe from the document without a key or manifest. + // This should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now run the subscribe from the service worker without a key. + // In this case, the sender id should be read from the datastore. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token2)); + EXPECT_EQ(token1, token2); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, subscribe again from the worker with no key. + // The sender id should again be read from the datastore, so the + // subcribe should succeed, and we should get a new subscription token. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token3)); + EXPECT_NE(token1, token3); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingFromDocumentWithP256Key) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPageWithoutManifest(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscription from the document with a key. + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result)); + + // Try to resubscribe from the document without a key - should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now try to resubscribe from the service worker without a key. + // This should also fail as the original key was not numeric. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and gcm_sender_id not found in manifest", + script_result); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, try to resubscribe again without a key. + // This should again fail. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and gcm_sender_id not found in manifest", + script_result); +} + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingFromWorkerWithP256Key) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPageWithoutManifest(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscribe from the service worker with a key. + // This should succeed. + ASSERT_TRUE(RunScript("workerSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, true /* standard_protocol */)); + + // Try to resubscribe from the document without a key - should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now try to resubscribe from the service worker without a key. + // This should also fail as the original key was not numeric. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, and " + "gcm_sender_id not found in manifest", + script_result); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, try to resubscribe again without a key. + // This should again fail. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and gcm_sender_id not found in manifest", + script_result); +} + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingFromDocumentWithNumber) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPageWithoutManifest(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscribe from the document with a numeric key. + // This should succeed. + ASSERT_TRUE( + RunScript("documentSubscribePushWithNumericKey()", &script_result)); + std::string token1; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token1)); + + // Try to resubscribe from the document without a key - should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now run the subscribe from the service worker without a key. + // In this case, the sender id should be read from the datastore. + // Note, we would rather this failed as we only really want to support + // no-key subscribes after subscribing with a numeric gcm sender id in the + // manifest, not a numeric applicationServerKey, but for code simplicity + // this case is allowed. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token2)); + EXPECT_EQ(token1, token2); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, subscribe again from the worker with no key. + // The sender id should again be read from the datastore, so the + // subcribe should succeed, and we should get a new subscription token. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token3)); + EXPECT_NE(token1, token3); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResubscribeWithoutKeyAfterSubscribingFromWorkerWithNumber) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPageWithoutManifest(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscribe from the service worker with a numeric key. + // This should succeed. + ASSERT_TRUE(RunScript("workerSubscribePushWithNumericKey()", &script_result)); + std::string token1; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token1)); + + // Try to resubscribe from the document without a key - should fail. + ASSERT_TRUE(RunScript("documentSubscribePushWithoutKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - missing applicationServerKey, " + "and manifest empty or missing", + script_result); + + // Now run the subscribe from the service worker without a key. + // In this case, the sender id should be read from the datastore. + // Note, we would rather this failed as we only really want to support + // no-key subscribes after subscribing with a numeric gcm sender id in the + // manifest, not a numeric applicationServerKey, but for code simplicity + // this case is allowed. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token2)); + EXPECT_EQ(token1, token2); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // After unsubscribing, subscribe again from the worker with no key. + // The sender id should again be read from the datastore, so the + // subcribe should succeed, and we should get a new subscription token. + ASSERT_TRUE(RunScript("workerSubscribePushNoKey()", &script_result)); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token3)); + EXPECT_NE(token1, token3); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, ResubscribeWithMismatchedKey) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Run the subscribe from the service worker with a key. + // This should succeed. + ASSERT_TRUE( + RunScript("workerSubscribePushWithNumericKey('11111')", &script_result)); + std::string token1; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token1)); + + // Try to resubscribe with a different key - should fail. + ASSERT_TRUE( + RunScript("workerSubscribePushWithNumericKey('22222')", &script_result)); + EXPECT_EQ( + "InvalidStateError - Registration failed - A subscription with a " + "different applicationServerKey (or gcm_sender_id) already exists; to " + "change the applicationServerKey, unsubscribe then resubscribe.", + script_result); + + // Try to resubscribe with the original key - should succeed. + ASSERT_TRUE( + RunScript("workerSubscribePushWithNumericKey('11111')", &script_result)); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token2)); + EXPECT_EQ(token1, token2); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // Resubscribe with a different key after unsubscribing. + // Should succeed, and we should get a new subscription token. + ASSERT_TRUE( + RunScript("workerSubscribePushWithNumericKey('22222')", &script_result)); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + EndpointToToken(script_result, false /* standard_protocol */, &token3)); + EXPECT_NE(token1, token3); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, SubscribePersisted) { + std::string script_result; + + // First, test that Service Worker registration IDs are assigned in order of + // registering the Service Workers, and the (fake) push subscription ids are + // assigned in order of push subscription (even when these orders are + // different). + + std::string token1; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token1)); + PushMessagingAppIdentifier sw0_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + EXPECT_EQ(sw0_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + LoadTestPage("/push_messaging/subscope1/test.html"); + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + LoadTestPage("/push_messaging/subscope2/test.html"); + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + // Note that we need to reload the page after registering, otherwise + // navigator.serviceWorker.ready is going to be resolved with the parent + // Service Worker which still controls the page. + LoadTestPage("/push_messaging/subscope2/test.html"); + std::string token2; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2)); + EXPECT_NE(token1, token2); + PushMessagingAppIdentifier sw2_identifier = + GetAppIdentifierForServiceWorkerRegistration(2LL); + EXPECT_EQ(sw2_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + LoadTestPage("/push_messaging/subscope1/test.html"); + std::string token3; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token3)); + EXPECT_NE(token1, token3); + EXPECT_NE(token2, token3); + PushMessagingAppIdentifier sw1_identifier = + GetAppIdentifierForServiceWorkerRegistration(1LL); + EXPECT_EQ(sw1_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + // Now test that the Service Worker registration IDs and push subscription IDs + // generated above were persisted to SW storage, by checking that they are + // unchanged despite requesting them in a different order. + + LoadTestPage("/push_messaging/subscope1/test.html"); + std::string token4; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token4)); + EXPECT_EQ(token3, token4); + EXPECT_EQ(sw1_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + LoadTestPage("/push_messaging/subscope2/test.html"); + std::string token5; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token5)); + EXPECT_EQ(token2, token5); + EXPECT_EQ(sw2_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + + LoadTestPage(); + std::string token6; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token6)); + EXPECT_EQ(token1, token6); + EXPECT_EQ(sw0_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, AppHandlerOnlyIfSubscribed) { + // This test restarts the push service to simulate restarting the browser. + + EXPECT_NE(push_service(), GetAppHandler()); + ASSERT_NO_FATAL_FAILURE(RestartPushService()); + EXPECT_NE(push_service(), GetAppHandler()); + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + EXPECT_EQ(push_service(), GetAppHandler()); + ASSERT_NO_FATAL_FAILURE(RestartPushService()); + EXPECT_EQ(push_service(), GetAppHandler()); + + std::string script_result; + + // Unsubscribe. + base::RunLoop run_loop; + push_service()->SetUnsubscribeCallbackForTesting(run_loop.QuitClosure()); + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + // The app handler is only guaranteed to be unregistered once the unsubscribe + // callback for testing has been run (PushSubscription.unsubscribe() usually + // resolves before that, in order to avoid blocking on network retries etc). + run_loop.Run(); + + EXPECT_NE(push_service(), GetAppHandler()); + ASSERT_NO_FATAL_FAILURE(RestartPushService()); + EXPECT_NE(push_service(), GetAppHandler()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventSuccess) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + push_service()->OnMessage(app_identifier.app_id(), message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true)); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("testdata", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.FindServiceWorker", + 0 /* SERVICE_WORKER_OK */, 1); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", + 0 /* SERVICE_WORKER_OK */, 1); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast<int>(blink::mojom::PushEventStatus::SUCCESS), 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventOnShutdown) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + push_service()->Observe(chrome::NOTIFICATION_APP_TERMINATING, + content::NotificationService::AllSources(), + content::NotificationService::NoDetails()); + push_service()->OnMessage(app_identifier.app_id(), message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventWithoutPayload) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.decrypted = false; + + push_service()->OnMessage(app_identifier.app_id(), message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("[NULL]", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, LegacyPushEvent) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + gcm::IncomingMessage message; + message.sender_id = kManifestSenderId; + message.decrypted = false; + + push_service()->OnMessage(app_identifier.app_id(), message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("[NULL]", script_result); +} + +// Some users may have gotten into a state in the past where they still have +// a subscription even though the service worker was unregistered. +// Emulate this and test a push message triggers unsubscription. +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventNoServiceWorker) { + std::string app_id; + ASSERT_NO_FATAL_FAILURE(SetupOrphanedPushSubscription(&app_id)); + + // Try to send a push message. + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + + base::RunLoop run_loop; + push_service()->SetMessageCallbackForTesting(run_loop.QuitClosure()); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + push_service()->OnMessage(app_id, message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true)); + run_loop.Run(); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + + // No push data should have been received. + std::string script_result; + ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.FindServiceWorker", + 5 /* SERVICE_WORKER_ERROR_NOT_FOUND */, 1); + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast<int>(blink::mojom::PushEventStatus::NO_SERVICE_WORKER), 1); + + // Missing Service Workers should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_id, gcm_driver_->last_deletetoken_app_id()); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::DELIVERY_NO_SERVICE_WORKER), + 1); + + // |app_identifier| should no longer be stored in prefs. + PushMessagingAppIdentifier stored_app_identifier = + PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), app_id); + EXPECT_TRUE(stored_app_identifier.is_null()); +} + +// Tests receiving messages for a subscription that no longer exists. +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, NoSubscription) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 1); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + SendMessageAndWaitUntilHandled(app_identifier, message); + + // No push data should have been received. + ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.FindServiceWorker", 0); + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast<int>(blink::mojom::PushEventStatus::UNKNOWN_APP_ID), 1); + + // Missing subscriptions should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id()); + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::DELIVERY_UNKNOWN_APP_ID), + 1); +} + +// Tests receiving messages for an origin that does not have permission, but +// somehow still has a subscription (as happened in https://crbug.com/633310). +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PushEventWithoutPermission) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Revoke notifications permission, but first disable the + // PushMessagingServiceImpl's OnContentSettingChanged handler so that it + // doesn't automatically unsubscribe, since we want to test the case where + // there is still a subscription. + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->RemoveObserver(push_service()); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS); + base::RunLoop().RunUntilIdle(); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + SendMessageAndWaitUntilHandled(app_identifier, message); + + // No push data should have been received. + ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.FindServiceWorker", 0); + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast<int>(blink::mojom::PushEventStatus::PERMISSION_DENIED), 1); + + // Missing permission should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id()); + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier_afterwards = + PushMessagingAppIdentifier::FindByServiceWorker(GetBrowser()->profile(), + origin, 0LL); + EXPECT_TRUE(app_identifier_afterwards.is_null()); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::DELIVERY_PERMISSION_DENIED), + 1); +} + +// https://crbug.com/458160 test is flaky on all platforms; but mostly linux. +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + DISABLED_PushEventEnforcesUserVisibleNotification) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + RemoveAllNotifications(); + ASSERT_EQ(0u, GetNotificationCount()); + + // We'll need to specify the web_contents in which to eval script, since we're + // going to run script in a background tab. + content::WebContents* web_contents = + GetBrowser()->tab_strip_model()->GetActiveWebContents(); + + // Set the site engagement score for the site. Setting it to 10 means it + // should have a budget of 4, enough for two non-shown notification, which + // cost 2 each. + SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 10.0); + + // If the site is visible in an active tab, we should not force a notification + // to be shown. Try it twice, since we allow one mistake per 10 push events. + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.decrypted = true; + for (int n = 0; n < 2; n++) { + message.raw_data = "testdata"; + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("testdata", script_result); + EXPECT_EQ(0u, GetNotificationCount()); + } + + // Open a blank foreground tab so site is no longer visible. + ui_test_utils::NavigateToURLWithDisposition( + GetBrowser(), GURL("about:blank"), + WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); + + // If the Service Worker push event handler shows a notification, we + // should not show a forced one. + message.raw_data = "shownotification"; + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("shownotification", script_result); + EXPECT_EQ(1u, GetNotificationCount()); + EXPECT_TRUE(TagEquals(GetDisplayedNotifications()[0], "push_test_tag")); + RemoveAllNotifications(); + + // If the Service Worker push event handler does not show a notification, we + // should show a forced one, but only once the origin is out of budget. + message.raw_data = "testdata"; + for (int n = 0; n < 2; n++) { + // First two missed notifications shouldn't force a default one. + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + EXPECT_EQ(0u, GetNotificationCount()); + } + + // Third missed notification should trigger a default notification, since the + // origin will be out of budget. + message.raw_data = "testdata"; + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + + { + std::vector<message_center::Notification> notifications = + GetDisplayedNotifications(); + ASSERT_EQ(notifications.size(), 1u); + + EXPECT_TRUE( + TagEquals(notifications[0], kPushMessagingForcedNotificationTag)); + EXPECT_TRUE(notifications[0].silent()); + } + + // The notification will be automatically dismissed when the developer shows + // a new notification themselves at a later point in time. + message.raw_data = "shownotification"; + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("shownotification", script_result); + + { + std::vector<message_center::Notification> notifications = + GetDisplayedNotifications(); + ASSERT_EQ(notifications.size(), 1u); + + EXPECT_FALSE( + TagEquals(notifications[0], kPushMessagingForcedNotificationTag)); + } +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + PushEventAllowSilentPushCommandLineFlag) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_gettoken_app_id()); + EXPECT_EQ(kEncodedApplicationServerKey, + gcm_driver_->last_gettoken_authorized_entity()); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + RemoveAllNotifications(); + ASSERT_EQ(0u, GetNotificationCount()); + + // We'll need to specify the web_contents in which to eval script, since we're + // going to run script in a background tab. + content::WebContents* web_contents = + GetBrowser()->tab_strip_model()->GetActiveWebContents(); + + SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 5.0); + + ui_test_utils::NavigateToURLWithDisposition( + GetBrowser(), GURL("about:blank"), + WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); + + // Send a missed notification to use up the budget. + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + EXPECT_EQ(0u, GetNotificationCount()); + + // If the Service Worker push event handler does not show a notification, we + // should show a forced one providing there is no foreground tab and the + // origin ran out of budget. + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + + // Because the --allow-silent-push command line flag has not been passed, + // this should have shown a default notification. + { + std::vector<message_center::Notification> notifications = + GetDisplayedNotifications(); + ASSERT_EQ(notifications.size(), 1u); + + EXPECT_TRUE( + TagEquals(notifications[0], kPushMessagingForcedNotificationTag)); + EXPECT_TRUE(notifications[0].silent()); + } + + RemoveAllNotifications(); + + // Send the message again, but this time with the -allow-silent-push command + // line flag set. The default notification should *not* be shown. + base::CommandLine::ForCurrentProcess()->AppendSwitch( + switches::kAllowSilentPush); + + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("testdata", script_result); + + ASSERT_EQ(0u, GetNotificationCount()); +} + +class PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation + : public PushMessagingBrowserTestBase { + public: + PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation() = default; + + using SiteReputation = CrowdDenyPreloadData::SiteReputation; + + void CreatedBrowserMainParts( + content::BrowserMainParts* browser_main_parts) override { + PushMessagingBrowserTestBase::CreatedBrowserMainParts(browser_main_parts); + + testing_preload_data_.emplace(); + fake_database_manager_ = + base::MakeRefCounted<CrowdDenyFakeSafeBrowsingDatabaseManager>(); + test_safe_browsing_factory_ = + std::make_unique<safe_browsing::TestSafeBrowsingServiceFactory>(); + test_safe_browsing_factory_->SetTestDatabaseManager( + fake_database_manager_.get()); + safe_browsing::SafeBrowsingServiceInterface::RegisterFactory( + test_safe_browsing_factory_.get()); + } + + void AddToPreloadDataBlocklist( + const GURL& origin, + chrome_browser_crowd_deny:: + SiteReputation_NotificationUserExperienceQuality reputation_type) { + SiteReputation reputation; + reputation.set_notification_ux_quality(reputation_type); + testing_preload_data_->SetOriginReputation(url::Origin::Create(origin), + std::move(reputation)); + } + + void AddToSafeBrowsingBlocklist(const GURL& url) { + safe_browsing::ThreatMetadata test_metadata; + test_metadata.api_permissions.emplace("NOTIFICATIONS"); + fake_database_manager_->SetSimulatedMetadataForUrl(url, test_metadata); + } + + private: + base::test::ScopedFeatureList feature_list_; + absl::optional<testing::ScopedCrowdDenyPreloadDataOverride> + testing_preload_data_; + scoped_refptr<CrowdDenyFakeSafeBrowsingDatabaseManager> + fake_database_manager_; + std::unique_ptr<safe_browsing::TestSafeBrowsingServiceFactory> + test_safe_browsing_factory_; +}; + +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation, + PushEventPermissionRevoked) { + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + std::string script_result; + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Add an origin to blocking lists after service worker is registered. + AddToPreloadDataBlocklist( + https_server()->GetURL("/").DeprecatedGetOriginAsURL(), + SiteReputation::ABUSIVE_CONTENT); + AddToSafeBrowsingBlocklist( + https_server()->GetURL("/").DeprecatedGetOriginAsURL()); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + SendMessageAndWaitUntilHandled(app_identifier, message); + + // No push data should have been received. + ASSERT_TRUE(RunScript("resultQueue.popImmediately()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.FindServiceWorker", 0); + histogram_tester_.ExpectTotalCount( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", 0); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast<int>( + blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE), + 1); + + // Missing permission should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_identifier.app_id(), gcm_driver_->last_deletetoken_app_id()); + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier_afterwards = + PushMessagingAppIdentifier::FindByServiceWorker(GetBrowser()->profile(), + origin, 0LL); + EXPECT_TRUE(app_identifier_afterwards.is_null()); + + // 1st event - blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED. + // 2nd event - + // blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE. + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 2); + + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE, 1); + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED, 1); +} + +// That test verifies that an origin is not revoked because it is not on +// SafeBrowsing blocking list. +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTestWithAbusiveOriginPermissionRevocation, + OriginIsNotOnSafeBrowsingBlockingList) { + std::string script_result; + + // The origin should be marked as |ABUSIVE_CONTENT| on |CrowdDenyPreloadData| + // otherwise the permission revocation logic will not be triggered. + AddToPreloadDataBlocklist( + https_server()->GetURL("/").DeprecatedGetOriginAsURL(), + SiteReputation::ABUSIVE_CONTENT); + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "testdata"; + message.decrypted = true; + push_service()->OnMessage(app_identifier.app_id(), message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true)); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("testdata", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.FindServiceWorker", + 0 /* SERVICE_WORKER_OK */, 1); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus.ServiceWorkerEvent", + 0 /* SERVICE_WORKER_OK */, 1); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.DeliveryStatus", + static_cast<int>(blink::mojom::PushEventStatus::SUCCESS), 1); +} + +class PushMessagingBrowserTestWithNotificationTriggersEnabled + : public PushMessagingBrowserTestBase { + public: + PushMessagingBrowserTestWithNotificationTriggersEnabled() { + feature_list_.InitAndEnableFeature(features::kNotificationTriggers); + } + + private: + base::test::ScopedFeatureList feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTestWithNotificationTriggersEnabled, + PushEventIgnoresScheduledNotificationsForEnforcement) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + LoadTestPage(); // Reload to become controlled. + + RemoveAllNotifications(); + + // We'll need to specify the web_contents in which to eval script, since we're + // going to run script in a background tab. + content::WebContents* web_contents = + GetBrowser()->tab_strip_model()->GetActiveWebContents(); + + // Initialize site engagement score to have no budget for silent pushes. + SetSiteEngagementScore(web_contents->GetLastCommittedURL(), 0); + + ui_test_utils::NavigateToURLWithDisposition( + GetBrowser(), GURL("about:blank"), + WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "shownotification-with-showtrigger"; + message.decrypted = true; + + // If the Service Worker push event handler only schedules a notification, we + // should show a forced one providing there is no foreground tab and the + // origin ran out of budget. + SendMessageAndWaitUntilHandled(app_identifier, message); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("shownotification-with-showtrigger", script_result); + + // Because scheduled notifications do not count as displayed notifications, + // this should have shown a default notification. + std::vector<message_center::Notification> notifications = + GetDisplayedNotifications(); + ASSERT_EQ(notifications.size(), 1u); + + EXPECT_TRUE(TagEquals(notifications[0], kPushMessagingForcedNotificationTag)); + EXPECT_TRUE(notifications[0].silent()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + PushEventEnforcesUserVisibleNotificationAfterQueue) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Fire off two push messages in sequence, only the second one of which will + // display a notification. The additional round-trip and I/O required by the + // second message, which shows a notification, should give us a reasonable + // confidence that the ordering will be maintained. + + std::vector<size_t> number_of_notifications_shown; + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.decrypted = true; + + { + base::RunLoop run_loop; + push_service()->SetMessageCallbackForTesting(base::BindRepeating( + &PushMessagingBrowserTestBase::OnDeliveryFinished, + base::Unretained(this), &number_of_notifications_shown, + base::BarrierClosure(2 /* num_closures */, run_loop.QuitClosure()))); + + message.raw_data = "testdata"; + push_service()->OnMessage(app_identifier.app_id(), message); + + message.raw_data = "shownotification"; + push_service()->OnMessage(app_identifier.app_id(), message); + + run_loop.Run(); + } + + ASSERT_EQ(2u, number_of_notifications_shown.size()); + EXPECT_EQ(0u, number_of_notifications_shown[0]); + EXPECT_EQ(1u, number_of_notifications_shown[1]); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + PushEventNotificationWithoutEventWaitUntil) { + std::string script_result; + content::WebContents* web_contents = + GetBrowser()->tab_strip_model()->GetActiveWebContents(); + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + base::RunLoop run_loop; + base::RepeatingClosure quit_barrier = + base::BarrierClosure(2 /* num_closures */, run_loop.QuitClosure()); + push_service()->SetMessageCallbackForTesting(quit_barrier); + notification_tester_->SetNotificationAddedClosure(quit_barrier); + + gcm::IncomingMessage message; + message.sender_id = GetTestApplicationServerKey(); + message.raw_data = "shownotification-without-waituntil"; + message.decrypted = true; + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + push_service()->OnMessage(app_identifier.app_id(), message); + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(true)); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); + EXPECT_EQ("immediate:shownotification-without-waituntil", script_result); + + run_loop.Run(); + + EXPECT_TRUE(IsRegisteredKeepAliveEqualTo(false)); + ASSERT_EQ(1u, GetNotificationCount()); + EXPECT_TRUE(TagEquals(GetDisplayedNotifications()[0], "push_test_tag")); + + // Verify that the renderer process hasn't crashed. + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysPrompt) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + ASSERT_EQ("permission status - prompt", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysGranted) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result)); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, PermissionStateSaysDenied) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndDenyPermission()); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + EXPECT_EQ("NotAllowedError - Registration failed - permission denied", + script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, CrossOriginFrame) { + const GURL kEmbedderURL = https_server()->GetURL( + "embedder.com", "/push_messaging/framed_test.html"); + const GURL kRequesterURL = https_server()->GetURL("requester.com", "/"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), kEmbedderURL)); + + auto* web_contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); + LOG(ERROR) << web_contents->GetLastCommittedURL(); + auto* subframe = content::ChildFrameAt(web_contents->GetMainFrame(), 0u); + ASSERT_TRUE(subframe); + + // A cross-origin subframe that had not been granted the NOTIFICATIONS + // permission previously should see it as "denied", not be able to request it, + // and not be able to use the Push and Web Notification API. It is verified + // that no prompts are shown by auto-accepting and still expecting the + // permission to be denied. + + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::ACCEPT_ALL); + + std::string script_result; + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "requestNotificationPermission();", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "notificationPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "notificationPermissionAPIState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "documentSubscribePush()", &script_result)); + EXPECT_EQ("NotAllowedError - Registration failed - permission denied", + script_result); + + // A cross-origin subframe that had been granted the NOTIFICATIONS permission + // previously (in a first-party context) should see it as "granted", and be + // able to use the Push and Web Notifications APIs. + + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(kRequesterURL, kRequesterURL, + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_ALLOW); + + GetPermissionRequestManager()->set_auto_response_for_test( + permissions::PermissionRequestManager::DENY_ALL); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "requestNotificationPermission();", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "notificationPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "notificationPermissionAPIState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + subframe, "documentSubscribePush()", &script_result)); + ASSERT_NO_FATAL_FAILURE(EndpointToToken(script_result)); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, UnsubscribeSuccess) { + std::string script_result; + + std::string token1; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token1)); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + + // Resolves true if there was a subscription. + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 1); + + // Resolves false if there was no longer a subscription. + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: false", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 2); + + // TODO(johnme): Test that doesn't reject if there was a network error (should + // deactivate subscription locally anyway). + // TODO(johnme): Test that doesn't reject if there were other push service + // errors (should deactivate subscription locally anyway). + + // Unsubscribing (with an existing reference to a PushSubscription), after + // replacing the Service Worker, actually still works, as the Service Worker + // registration is unchanged. + std::string token2; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token2)); + EXPECT_NE(token1, token2); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + ASSERT_TRUE(RunScript("replaceServiceWorker()", &script_result)); + EXPECT_EQ("ok - service worker replaced", script_result); + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 3); + + // Unsubscribing (with an existing reference to a PushSubscription), after + // unregistering the Service Worker, should fail. + std::string token3; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token3)); + EXPECT_NE(token1, token3); + EXPECT_NE(token2, token3); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + + // Unregister service worker and wait for callback. + base::RunLoop run_loop; + push_service()->SetServiceWorkerUnregisteredCallbackForTesting( + run_loop.QuitClosure()); + ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result)); + EXPECT_EQ("service worker unregistration status: true", script_result); + run_loop.Run(); + + // Unregistering should have triggered an automatic unsubscribe. + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED), + 1); + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 4); + + // Now manual unsubscribe should return false. + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: false", script_result); +} + +// Push subscriptions used to be non-InstanceID GCM registrations. Still need +// to be able to unsubscribe these, even though new ones are no longer created. +// Flaky on some Win and Linux buildbots. See crbug.com/835382. +#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) +#define MAYBE_LegacyUnsubscribeSuccess DISABLED_LegacyUnsubscribeSuccess +#else +#define MAYBE_LegacyUnsubscribeSuccess LegacyUnsubscribeSuccess +#endif +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + MAYBE_LegacyUnsubscribeSuccess) { + std::string script_result; + + std::string subscription_id1; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id1)); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + + // Resolves true if there was a subscription. + gcm_service_->AddExpectedUnregisterResponse(gcm::GCMClient::SUCCESS); + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 1); + + // Resolves false if there was no longer a subscription. + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: false", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 2); + + // Doesn't reject if there was a network error (deactivates subscription + // locally anyway). + std::string subscription_id2; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id2)); + EXPECT_NE(subscription_id1, subscription_id2); + gcm_service_->AddExpectedUnregisterResponse(gcm::GCMClient::NETWORK_ERROR); + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 3); + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + // Doesn't reject if there were other push service errors (deactivates + // subscription locally anyway). + std::string subscription_id3; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id3)); + EXPECT_NE(subscription_id1, subscription_id3); + EXPECT_NE(subscription_id2, subscription_id3); + gcm_service_->AddExpectedUnregisterResponse( + gcm::GCMClient::INVALID_PARAMETER); + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 4); + + // Unsubscribing (with an existing reference to a PushSubscription), after + // replacing the Service Worker, actually still works, as the Service Worker + // registration is unchanged. + std::string subscription_id4; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id4)); + EXPECT_NE(subscription_id1, subscription_id4); + EXPECT_NE(subscription_id2, subscription_id4); + EXPECT_NE(subscription_id3, subscription_id4); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + ASSERT_TRUE(RunScript("replaceServiceWorker()", &script_result)); + EXPECT_EQ("ok - service worker replaced", script_result); + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 5); + + // Unsubscribing (with an existing reference to a PushSubscription), after + // unregistering the Service Worker, should fail. + std::string subscription_id5; + ASSERT_NO_FATAL_FAILURE(LegacySubscribeSuccessfully(&subscription_id5)); + EXPECT_NE(subscription_id1, subscription_id5); + EXPECT_NE(subscription_id2, subscription_id5); + EXPECT_NE(subscription_id3, subscription_id5); + EXPECT_NE(subscription_id4, subscription_id5); + ASSERT_TRUE(RunScript("storePushSubscription()", &script_result)); + EXPECT_EQ("ok - stored", script_result); + + // Unregister service worker and wait for callback. + base::RunLoop run_loop; + push_service()->SetServiceWorkerUnregisteredCallbackForTesting( + run_loop.QuitClosure()); + ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result)); + EXPECT_EQ("service worker unregistration status: true", script_result); + run_loop.Run(); + + // Unregistering should have triggered an automatic unsubscribe. + histogram_tester_.ExpectBucketCount( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED), + 1); + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 6); + + // Now manual unsubscribe should return false. + ASSERT_TRUE(RunScript("unsubscribeStoredPushSubscription()", &script_result)); + EXPECT_EQ("unsubscribe result: false", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, UnsubscribeOffline) { + std::string script_result; + + EXPECT_NE(push_service(), GetAppHandler()); + + std::string token; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token)); + + gcm_service_->set_offline(true); + + // Should quickly resolve true after deleting local state (rather than waiting + // until unsubscribing over the network exceeds the maximum backoff duration). + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason::JAVASCRIPT_API), + 1); + + // Since the service is offline, the network request to GCM is still being + // retried, so the app handler shouldn't have been unregistered yet. + EXPECT_EQ(push_service(), GetAppHandler()); + // But restarting the push service will unregister the app handler, since the + // subscription is no longer stored in the PushMessagingAppIdentifier map. + ASSERT_NO_FATAL_FAILURE(RestartPushService()); + EXPECT_NE(push_service(), GetAppHandler()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + UnregisteringServiceWorkerUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Unregister the worker, and wait for callback to complete. + base::RunLoop run_loop; + push_service()->SetServiceWorkerUnregisteredCallbackForTesting( + run_loop.QuitClosure()); + ASSERT_TRUE(RunScript("unregisterServiceWorker()", &script_result)); + ASSERT_EQ("service worker unregistration status: true", script_result); + run_loop.Run(); + + // This should have unregistered the push subscription. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED), + 1); + + // We should not be able to look up the app id. + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, + 0LL /* service_worker_registration_id */); + EXPECT_TRUE(app_identifier.is_null()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + ServiceWorkerDatabaseDeletionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Pretend as if the Service Worker database went away, and wait for callback + // to complete. + base::RunLoop run_loop; + push_service()->SetServiceWorkerDatabaseWipedCallbackForTesting( + run_loop.QuitClosure()); + push_service()->DidDeleteServiceWorkerDatabase(); + run_loop.Run(); + + // This should have unregistered the push subscription. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason:: + SERVICE_WORKER_DATABASE_WIPED), + 1); + + // There should not be any subscriptions left. + EXPECT_EQ(PushMessagingAppIdentifier::GetCount(GetBrowser()->profile()), 0u); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + InvalidGetSubscriptionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + PushMessagingAppIdentifier app_identifier1 = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, + 0LL /* service_worker_registration_id */); + ASSERT_FALSE(app_identifier1.is_null()); + + ASSERT_NO_FATAL_FAILURE( + DeleteInstanceIDAsIfGCMStoreReset(app_identifier1.app_id())); + + // Push messaging should not yet be aware of the InstanceID being deleted. + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0); + // We should still be able to look up the app id. + PushMessagingAppIdentifier app_identifier2 = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, + 0LL /* service_worker_registration_id */); + EXPECT_FALSE(app_identifier2.is_null()); + EXPECT_EQ(app_identifier1.app_id(), app_identifier2.app_id()); + + // Now call PushManager.getSubscription(). It should return null. + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + // This should have unsubscribed the push subscription. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>(blink::mojom::PushUnregistrationReason:: + GET_SUBSCRIPTION_STORAGE_CORRUPT), + 1); + // We should no longer be able to look up the app id. + PushMessagingAppIdentifier app_identifier3 = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), origin, + 0LL /* service_worker_registration_id */); + EXPECT_TRUE(app_identifier3.is_null()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + GlobalResetPushPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr<content::MessageLoopRunner> message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - prompt", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + LocalResetPushPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr<content::MessageLoopRunner> message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, origin, + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_DEFAULT); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - prompt", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + DenyPushPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr<content::MessageLoopRunner> message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, origin, + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_BLOCK); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + GlobalResetNotificationsPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr<content::MessageLoopRunner> message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - prompt", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + LocalResetNotificationsPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr<content::MessageLoopRunner> message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_DEFAULT); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - prompt", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + DenyNotificationsPermissionUnsubscribes) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr<content::MessageLoopRunner> message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_BLOCK); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("false - not subscribed", script_result); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + GrantAlreadyGrantedPermissionDoesNotUnsubscribe) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr<content::MessageLoopRunner> message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(1, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_ALLOW); + + message_loop_runner->Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0); +} + +// This test is testing some non-trivial content settings rules and make sure +// that they are respected with regards to automatic unsubscription. In other +// words, it checks that the push service does not end up unsubscribing origins +// that have push permission with some non-common rules. +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, + AutomaticUnsubscriptionFollowsContentSettingRules) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + scoped_refptr<content::MessageLoopRunner> message_loop_runner = + new content::MessageLoopRunner; + push_service()->SetContentSettingChangedCallbackForTesting( + base::BarrierClosure(2, message_loop_runner->QuitClosure())); + + GURL origin = https_server()->GetURL("/").DeprecatedGetOriginAsURL(); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetDefaultContentSetting(ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_ALLOW); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(origin, GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_DEFAULT); + + message_loop_runner->Run(); + + // The two first rules should give |origin| the permission to use Push even + // if the rules it used to have have been reset. + // The Push service should not unsubscribe |origin| because at no point it was + // left without permission to use Push. + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + histogram_tester_.ExpectTotalCount("PushMessaging.UnregistrationReason", 0); +} + +// Checks automatically unsubscribing due to a revoked permission after +// previously clearing site data, under legacy conditions (ie. when +// unregistering a worker did not unsubscribe from push.) +IN_PROC_BROWSER_TEST_F( + PushMessagingBrowserTest, + ResetPushPermissionAfterClearingSiteDataUnderLegacyConditions) { + std::string app_id; + ASSERT_NO_FATAL_FAILURE(SetupOrphanedPushSubscription(&app_id)); + + // Simulate a user clearing site data (including Service Workers, crucially). + content::BrowsingDataRemover* remover = + GetBrowser()->profile()->GetBrowsingDataRemover(); + content::BrowsingDataRemoverCompletionObserver observer(remover); + remover->RemoveAndReply( + base::Time(), base::Time::Max(), + chrome_browsing_data_remover::DATA_TYPE_SITE_DATA, + content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB, &observer); + observer.BlockUntilCompletion(); + + base::RunLoop run_loop; + push_service()->SetContentSettingChangedCallbackForTesting( + run_loop.QuitClosure()); + // This shouldn't (asynchronously) cause a DCHECK. + // TODO(johnme): Get this test running on Android with legacy GCM + // registrations, which have a different codepath due to sender_id being + // required for unsubscribing there. + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->ClearSettingsForOneType(ContentSettingsType::NOTIFICATIONS); + + run_loop.Run(); + + // |app_identifier| should no longer be stored in prefs. + PushMessagingAppIdentifier stored_app_identifier = + PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), app_id); + EXPECT_TRUE(stored_app_identifier.is_null()); + + histogram_tester_.ExpectUniqueSample( + "PushMessaging.UnregistrationReason", + static_cast<int>( + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED), + 1); + + base::RunLoop().RunUntilIdle(); + + // Revoked permission should trigger an automatic unsubscription attempt. + EXPECT_EQ(app_id, gcm_driver_->last_deletetoken_app_id()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, EncryptionKeyUniqueness) { + std::string token1; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kOmitKey, &token1)); + + std::string first_public_key; + ASSERT_TRUE(RunScript("GetP256dh()", &first_public_key)); + EXPECT_GE(first_public_key.size(), 32u); + + std::string script_result; + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + std::string token2; + ASSERT_NO_FATAL_FAILURE( + SubscribeSuccessfully(PushSubscriptionKeyFormat::kBinary, &token2)); + EXPECT_NE(token1, token2); + + std::string second_public_key; + ASSERT_TRUE(RunScript("GetP256dh()", &second_public_key)); + EXPECT_GE(second_public_key.size(), 32u); + + EXPECT_NE(first_public_key, second_public_key); +} + +class PushMessagingIncognitoBrowserTest : public PushMessagingBrowserTestBase { + public: + PushMessagingIncognitoBrowserTest() + : prerender_helper_(base::BindRepeating( + &PushMessagingIncognitoBrowserTest::web_contents, + base::Unretained(this))) {} + ~PushMessagingIncognitoBrowserTest() override = default; + + // PushMessagingBrowserTest: + void SetUpOnMainThread() override { + incognito_browser_ = CreateIncognitoBrowser(); + // We SetUp here rather than in SetUp since the https_server isn't yet + // created at that time. + prerender_helper_.SetUp(https_server()); + PushMessagingBrowserTestBase::SetUpOnMainThread(); + } + Browser* GetBrowser() const override { return incognito_browser_; } + + content::WebContents* web_contents() { + return GetBrowser()->tab_strip_model()->GetActiveWebContents(); + } + + protected: + content::test::PrerenderTestHelper prerender_helper_; + raw_ptr<Browser> incognito_browser_ = nullptr; +}; + +// Regression test for https://crbug.com/476474 +IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest, + IncognitoGetSubscriptionDoesNotHang) { + ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord()); + + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + // In Incognito mode the promise returned by getSubscription should not hang, + // it should just fulfill with null. + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + ASSERT_EQ("false - not subscribed", script_result); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest, WarningToCorrectRFH) { + ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord()); + + content::WebContentsConsoleObserver console_observer(web_contents()); + console_observer.SetPattern(kIncognitoWarningPattern); + + // Filter out the main frame host of the currently active page. + content::RenderFrameHost* rfh = web_contents()->GetMainFrame(); + console_observer.SetFilter(base::BindLambdaForTesting( + [&](const content::WebContentsConsoleObserver::Message& message) { + return message.source_frame == rfh; + })); + + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_TRUE(RunScript("documentSubscribePush()", &script_result)); + ASSERT_EQ("AbortError - Registration failed - permission denied", + script_result); + + console_observer.Wait(); + EXPECT_EQ(1u, console_observer.messages().size()); +} + +IN_PROC_BROWSER_TEST_F(PushMessagingIncognitoBrowserTest, + WarningToCorrectRFH_Prerender) { + ASSERT_TRUE(GetBrowser()->profile()->IsOffTheRecord()); + + const GURL url(https_server()->GetURL(GetTestURL())); + + // Start a prerender with the push messaging test URL. + int host_id = prerender_helper_.AddPrerender(url); + content::test::PrerenderHostObserver prerender_observer(*web_contents(), + host_id); + ASSERT_NE(prerender_helper_.GetHostForUrl(url), + content::RenderFrameHost::kNoFrameTreeNodeId); + + content::WebContentsConsoleObserver console_observer(web_contents()); + console_observer.SetPattern(kIncognitoWarningPattern); + + // Filter out the main frame host of the prerendered page. + content::RenderFrameHost* prerender_rfh = + prerender_helper_.GetPrerenderedMainFrameHost(host_id); + console_observer.SetFilter(base::BindLambdaForTesting( + [&](const content::WebContentsConsoleObserver::Message& message) { + return message.source_frame == prerender_rfh; + })); + + std::string script_result; + + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + prerender_rfh, "registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + // Use ExecuteScriptAsync because binding of blink::mojom::PushMessaging + // is deferred for the prerendered page. Script execution will finish after + // the activation. + ExecuteScriptAsync(prerender_rfh, "documentSubscribePush()"); + + // Activate the prerendered page and wait for a response of script execution. + content::DOMMessageQueue message_queue; + prerender_helper_.NavigatePrimaryPage(url); + // Make sure that the prerender was activated. + ASSERT_TRUE(prerender_observer.was_activated()); + do { + ASSERT_TRUE(message_queue.WaitForMessage(&script_result)); + } while (script_result != + "\"AbortError - Registration failed - permission denied\""); + + console_observer.Wait(); + EXPECT_EQ(1u, console_observer.messages().size()); +} + +class PushMessagingDisallowSenderIdsBrowserTest + : public PushMessagingBrowserTestBase { + public: + PushMessagingDisallowSenderIdsBrowserTest() { + scoped_feature_list_.InitAndEnableFeature( + features::kPushMessagingDisallowSenderIDs); + } + + ~PushMessagingDisallowSenderIdsBrowserTest() override = default; + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushMessagingDisallowSenderIdsBrowserTest, + SubscriptionWithSenderIdFails) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Attempt to create a subscription with a GCM Sender ID ("numeric key"), + // which should fail because the kPushMessagingDisallowSenderIDs feature has + // been enabled for this test. + ASSERT_TRUE( + RunScript("documentSubscribePushWithNumericKey()", &script_result)); + EXPECT_EQ( + "AbortError - Registration failed - GCM Sender IDs are no longer " + "supported, please upgrade to VAPID authentication instead", + script_result); +} + +class PushSubscriptionWithExpirationTimeTest + : public PushMessagingBrowserTestBase { + public: + PushSubscriptionWithExpirationTimeTest() { + scoped_feature_list_.InitAndEnableFeature( + features::kPushSubscriptionWithExpirationTime); + } + + ~PushSubscriptionWithExpirationTimeTest() override = default; + + // Checks whether |expiration_time| lies in the future and is in the + // valid format (seconds elapsed since Unix time) + bool IsExpirationTimeValid(const std::string& expiration_time); + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +bool PushSubscriptionWithExpirationTimeTest::IsExpirationTimeValid( + const std::string& expiration_time) { + int64_t output; + if (!base::StringToInt64(expiration_time, &output)) + return false; + return base::Time::Now().ToJsTimeIgnoringNull() < output; +} + +IN_PROC_BROWSER_TEST_F(PushSubscriptionWithExpirationTimeTest, + SubscribeGetSubscriptionWithExpirationTime) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // Subscribe with expiration time enabled, should get a subscription with + // expiration time in the future back + std::string subscription_expiration_time; + ASSERT_TRUE(RunScript("documentSubscribePushGetExpirationTime()", + &subscription_expiration_time)); + EXPECT_TRUE(IsExpirationTimeValid(subscription_expiration_time)); + + std::string get_subscription_expiration_time; + // Get subscription should also yield a subscription with expiration time + ASSERT_TRUE(RunScript("GetSubscriptionExpirationTime()", + &get_subscription_expiration_time)); + EXPECT_TRUE(IsExpirationTimeValid(get_subscription_expiration_time)); + // Both methods should return the same expiration time + ASSERT_EQ(subscription_expiration_time, get_subscription_expiration_time); +} + +IN_PROC_BROWSER_TEST_F(PushSubscriptionWithExpirationTimeTest, + GetSubscriptionWithExpirationTime) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + // Get subscription should also yield a subscription with expiration time + ASSERT_TRUE(RunScript("GetSubscriptionExpirationTime()", &script_result)); + EXPECT_TRUE(IsExpirationTimeValid(script_result)); +} + +class PushSubscriptionWithoutExpirationTimeTest + : public PushMessagingBrowserTestBase { + public: + PushSubscriptionWithoutExpirationTimeTest() { + // Override current feature list to ensure having + // |kPushSubscriptionWithExpirationTime| disabled + scoped_feature_list_.InitAndDisableFeature( + features::kPushSubscriptionWithExpirationTime); + } + + ~PushSubscriptionWithoutExpirationTimeTest() override = default; + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushSubscriptionWithoutExpirationTimeTest, + SubscribeDocumentExpirationTimeNull) { + std::string script_result; + + ASSERT_TRUE(RunScript("registerServiceWorker()", &script_result)); + ASSERT_EQ("ok - service worker registered", script_result); + + ASSERT_NO_FATAL_FAILURE(RequestAndAcceptPermission()); + + LoadTestPage(); // Reload to become controlled. + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + // When |features::kPushSubscriptionWithExpirationTime| is disabled, + // expiration time should be null + ASSERT_TRUE( + RunScript("documentSubscribePushGetExpirationTime()", &script_result)); + EXPECT_EQ("null", script_result); +} + +class PushSubscriptionChangeEventTest : public PushMessagingBrowserTestBase { + public: + PushSubscriptionChangeEventTest() { + scoped_feature_list_.InitWithFeatures( + {features::kPushSubscriptionChangeEvent, + features::kPushSubscriptionWithExpirationTime}, + {}); + } + + ~PushSubscriptionChangeEventTest() override = default; + + private: + base::test::ScopedFeatureList scoped_feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest, + PushSubscriptionChangeEventSuccess) { + std::string script_result; + + // Create the |old_subscription| by subscribing and unsubscribing again + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + + blink::mojom::PushSubscriptionPtr old_subscription = + GetSubscriptionForAppIdentifier(app_identifier); + + ASSERT_TRUE(RunScript("unsubscribePush()", &script_result)); + EXPECT_EQ("unsubscribe result: true", script_result); + + // There should be no subscription since we unsubscribed + EXPECT_EQ(PushMessagingAppIdentifier::GetCount(GetBrowser()->profile()), 0u); + + // Create a |new_subscription| by resubscribing + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + app_identifier = GetAppIdentifierForServiceWorkerRegistration(0LL); + + blink::mojom::PushSubscriptionPtr new_subscription = + GetSubscriptionForAppIdentifier(app_identifier); + + // Save the endpoints to compare with the JS result + GURL old_endpoint = old_subscription->endpoint; + GURL new_endpoint = new_subscription->endpoint; + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + base::RunLoop run_loop; + push_service()->FirePushSubscriptionChange( + app_identifier, run_loop.QuitClosure(), std::move(new_subscription), + std::move(old_subscription)); + run_loop.Run(); + + // Compare old subscription + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ(old_endpoint.spec(), script_result); + // Compare new subscription + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ(new_endpoint.spec(), script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.PushSubscriptionChangeStatus", + blink::mojom::PushEventStatus::SUCCESS, 1); +} + +IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest, + FiredAfterPermissionRevoked) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - granted", script_result); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + auto old_subscription = GetSubscriptionForAppIdentifier(app_identifier); + + base::RunLoop run_loop; + push_service()->SetContentSettingChangedCallbackForTesting( + run_loop.QuitClosure()); + HostContentSettingsMapFactory::GetForProfile(GetBrowser()->profile()) + ->SetContentSettingDefaultScope(app_identifier.origin(), GURL(), + ContentSettingsType::NOTIFICATIONS, + CONTENT_SETTING_BLOCK); + run_loop.Run(); + + ASSERT_TRUE(RunScript("pushManagerPermissionState()", &script_result)); + EXPECT_EQ("permission status - denied", script_result); + + // Check if the pushsubscriptionchangeevent arrived in the document and + // whether the |old_subscription| has the expected endpoint and + // |new_subscription| is null + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ(old_subscription->endpoint.spec(), script_result); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_EQ("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.PushSubscriptionChangeStatus", + blink::mojom::PushEventStatus::SUCCESS, 1); +} + +IN_PROC_BROWSER_TEST_F(PushSubscriptionChangeEventTest, OnInvalidation) { + std::string script_result; + + ASSERT_NO_FATAL_FAILURE(SubscribeSuccessfully()); + + ASSERT_TRUE(RunScript("hasSubscription()", &script_result)); + EXPECT_EQ("true - subscribed", script_result); + + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("false - is not controlled", script_result); + LoadTestPage(); // Reload to become controlled. + ASSERT_TRUE(RunScript("isControlled()", &script_result)); + ASSERT_EQ("true - is controlled", script_result); + + PushMessagingAppIdentifier app_identifier = + GetAppIdentifierForServiceWorkerRegistration(0LL); + ASSERT_FALSE(app_identifier.is_null()); + + base::RunLoop run_loop; + push_service()->SetInvalidationCallbackForTesting(run_loop.QuitClosure()); + push_service()->OnSubscriptionInvalidation(app_identifier.app_id()); + run_loop.Run(); + + // Old subscription should be gone + PushMessagingAppIdentifier deleted_identifier = + PushMessagingAppIdentifier::FindByAppId(GetBrowser()->profile(), + app_identifier.app_id()); + EXPECT_TRUE(deleted_identifier.is_null()); + + // New subscription with a different app id should exist + PushMessagingAppIdentifier new_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + GetBrowser()->profile(), app_identifier.origin(), + app_identifier.service_worker_registration_id()); + EXPECT_FALSE(new_identifier.is_null()); + + base::RunLoop().RunUntilIdle(); + + // Expect `pushsubscriptionchange` event that is not null + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_NE("null", script_result); + ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); + EXPECT_NE("null", script_result); + + // Check that we record this case in UMA. + histogram_tester_.ExpectUniqueSample( + "PushMessaging.PushSubscriptionChangeStatus", + blink::mojom::PushEventStatus::SUCCESS, 1); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_constants.cc b/chromium/chrome/browser/push_messaging/push_messaging_constants.cc new file mode 100644 index 00000000000..bd0d33d6134 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_constants.cc @@ -0,0 +1,11 @@ +// Copyright 2014 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. + +#include "chrome/browser/push_messaging/push_messaging_constants.h" + +const char kPushMessagingGcmEndpoint[] = + "https://fcm.googleapis.com/fcm/send/"; + +const char kPushMessagingForcedNotificationTag[] = + "user_visible_auto_notification"; diff --git a/chromium/chrome/browser/push_messaging/push_messaging_constants.h b/chromium/chrome/browser/push_messaging/push_messaging_constants.h new file mode 100644 index 00000000000..cf0480a5f14 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_constants.h @@ -0,0 +1,25 @@ +// Copyright 2014 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_ + +#include "base/time/time.h" + +extern const char kPushMessagingGcmEndpoint[]; + +// The tag of the notification that will be automatically shown if a webapp +// receives a push message then fails to show a notification. +extern const char kPushMessagingForcedNotificationTag[]; + +// Chrome decided cadence on subscription refreshes. According to the standards: +// https://w3c.github.io/push-api/#dfn-subscription-expiration-time it is +// optional and set by the browser. +constexpr base::TimeDelta kPushSubscriptionExpirationPeriodTimeDelta = + base::Days(90); + +// TimeDelta for subscription refreshes to keep old subscriptions alive +constexpr base::TimeDelta kPushSubscriptionRefreshTimeDelta = base::Minutes(2); + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_CONSTANTS_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_features.cc b/chromium/chrome/browser/push_messaging/push_messaging_features.cc new file mode 100644 index 00000000000..11ad3a97bb5 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_features.cc @@ -0,0 +1,15 @@ +// 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. + +#include "chrome/browser/push_messaging/push_messaging_features.h" + +namespace features { + +const base::Feature kPushMessagingDisallowSenderIDs{ + "PushMessagingDisallowSenderIDs", base::FEATURE_DISABLED_BY_DEFAULT}; + +const base::Feature kPushSubscriptionWithExpirationTime{ + "PushSubscriptionWithExpirationTime", base::FEATURE_DISABLED_BY_DEFAULT}; + +} // namespace features diff --git a/chromium/chrome/browser/push_messaging/push_messaging_features.h b/chromium/chrome/browser/push_messaging/push_messaging_features.h new file mode 100644 index 00000000000..1bdc9237860 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_features.h @@ -0,0 +1,21 @@ +// 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_ + +#include "base/feature_list.h" + +namespace features { + +// Feature flag to disallow creation of push messages with GCM Sender IDs. +extern const base::Feature kPushMessagingDisallowSenderIDs; + +// Feature flag to enable push subscription with expiration times specified in +// /chrome/browser/push_messaging/push_messaging_constants.h +extern const base::Feature kPushSubscriptionWithExpirationTime; + +} // namespace features + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_FEATURES_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc new file mode 100644 index 00000000000..e2a8f2adc42 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.cc @@ -0,0 +1,353 @@ +// 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. + +#include "chrome/browser/push_messaging/push_messaging_notification_manager.h" + +#include <stddef.h> + +#include <bitset> +#include <utility> + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/feature_list.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/utf_string_conversions.h" +#include "base/task/post_task.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/notifications/platform_notification_service_factory.h" +#include "chrome/browser/notifications/platform_notification_service_impl.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "chrome/grit/generated_resources.h" +#include "components/site_engagement/content/site_engagement_service.h" +#include "components/url_formatter/elide_url.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/platform_notification_context.h" +#include "content/public/browser/push_messaging_service.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_features.h" +#include "content/public/common/page_visibility_state.h" +#include "content/public/common/url_constants.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "third_party/blink/public/common/notifications/notification_resources.h" +#include "third_party/blink/public/mojom/notifications/notification.mojom-shared.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/gurl.h" + +#if defined(OS_ANDROID) +#include "chrome/browser/ui/android/tab_model/tab_model.h" +#include "chrome/browser/ui/android/tab_model/tab_model_list.h" +#else +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/android_sms/android_sms_service_factory.h" +#include "chrome/browser/ash/android_sms/android_sms_urls.h" +#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h" +#endif + +using content::BrowserThread; +using content::NotificationDatabaseData; +using content::PlatformNotificationContext; +using content::PushMessagingService; +using content::ServiceWorkerContext; +using content::WebContents; + +namespace { +void RecordUserVisibleStatus(blink::mojom::PushUserVisibleStatus status) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.UserVisibleStatus", status); +} + +content::StoragePartition* GetStoragePartition(Profile* profile, + const GURL& origin) { + return profile->GetStoragePartitionForUrl(origin); +} + +NotificationDatabaseData CreateDatabaseData( + const GURL& origin, + int64_t service_worker_registration_id) { + blink::PlatformNotificationData notification_data; + notification_data.title = url_formatter::FormatUrlForSecurityDisplay( + origin, url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS); + notification_data.direction = + blink::mojom::NotificationDirection::LEFT_TO_RIGHT; + notification_data.body = + l10n_util::GetStringUTF16(IDS_PUSH_MESSAGING_GENERIC_NOTIFICATION_BODY); + notification_data.tag = kPushMessagingForcedNotificationTag; + notification_data.icon = GURL(); + notification_data.timestamp = base::Time::Now(); + notification_data.silent = true; + + NotificationDatabaseData database_data; + database_data.origin = origin; + database_data.service_worker_registration_id = service_worker_registration_id; + database_data.notification_data = notification_data; + + // Make sure we don't expose this notification to the site. + database_data.is_shown_by_browser = true; + + return database_data; +} + +} // namespace + +PushMessagingNotificationManager::PushMessagingNotificationManager( + Profile* profile) + : profile_(profile), budget_database_(profile) {} + +PushMessagingNotificationManager::~PushMessagingNotificationManager() = default; + +void PushMessagingNotificationManager::EnforceUserVisibleOnlyRequirements( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (ShouldSkipUserVisibleOnlyRequirements(origin)) { + std::move(message_handled_callback) + .Run(/* did_show_generic_notification= */ false); + return; + } +#endif + + // TODO(johnme): Relax this heuristic slightly. + scoped_refptr<PlatformNotificationContext> notification_context = + GetStoragePartition(profile_, origin)->GetPlatformNotificationContext(); + + notification_context->CountVisibleNotificationsForServiceWorkerRegistration( + origin, service_worker_registration_id, + base::BindOnce( + &PushMessagingNotificationManager::DidCountVisibleNotifications, + weak_factory_.GetWeakPtr(), origin, service_worker_registration_id, + std::move(message_handled_callback))); +} + +void PushMessagingNotificationManager::DidCountVisibleNotifications( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback, + bool success, + int notification_count) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + // TODO(johnme): Hiding an existing notification should also count as a useful + // user-visible action done in response to a push message - but make sure that + // sending two messages in rapid succession which show then hide a + // notification doesn't count. + // TODO(crbug.com/891339): Scheduling a notification should count as a + // user-visible action, if it is not immediately cancelled or the |origin| + // schedules too many notifications too far in the future. + bool notification_shown = notification_count > 0; + bool notification_needed = true; + + base::UmaHistogramCounts100("PushMessaging.VisibleNotificationCount", + notification_count); + + // Sites with a currently visible tab don't need to show notifications. +#if defined(OS_ANDROID) + for (const TabModel* model : TabModelList::models()) { + Profile* profile = model->GetProfile(); + WebContents* active_web_contents = model->GetActiveWebContents(); +#else + for (auto* browser : *BrowserList::GetInstance()) { + Profile* profile = browser->profile(); + WebContents* active_web_contents = + browser->tab_strip_model()->GetActiveWebContents(); +#endif + if (IsTabVisible(profile, active_web_contents, origin)) { + notification_needed = false; + break; + } + } + + // If more than one notification is showing for this Service Worker, close + // the default notification if it happens to be part of this group. + if (notification_count >= 2) { + scoped_refptr<PlatformNotificationContext> notification_context = + GetStoragePartition(profile_, origin)->GetPlatformNotificationContext(); + notification_context->DeleteAllNotificationDataWithTag( + kPushMessagingForcedNotificationTag, /*is_shown_by_browser=*/true, + origin, base::DoNothing()); + } + + if (notification_needed && !notification_shown) { + // If the worker needed to show a notification and didn't, see if a silent + // push was allowed. + budget_database_.SpendBudget( + url::Origin::Create(origin), + base::BindOnce(&PushMessagingNotificationManager::ProcessSilentPush, + weak_factory_.GetWeakPtr(), origin, + service_worker_registration_id, + std::move(message_handled_callback))); + return; + } + + if (notification_needed && notification_shown) { + RecordUserVisibleStatus( + blink::mojom::PushUserVisibleStatus::REQUIRED_AND_SHOWN); + } else if (!notification_needed && !notification_shown) { + RecordUserVisibleStatus( + blink::mojom::PushUserVisibleStatus::NOT_REQUIRED_AND_NOT_SHOWN); + } else { + RecordUserVisibleStatus( + blink::mojom::PushUserVisibleStatus::NOT_REQUIRED_BUT_SHOWN); + } + + std::move(message_handled_callback) + .Run(/* did_show_generic_notification= */ false); +} + +bool PushMessagingNotificationManager::IsTabVisible( + Profile* profile, + WebContents* active_web_contents, + const GURL& origin) { + if (!active_web_contents || !active_web_contents->GetMainFrame()) + return false; + + // Don't leak information from other profiles. + if (profile != profile_) + return false; + + // Ignore minimized windows. + switch (active_web_contents->GetMainFrame()->GetVisibilityState()) { + case content::PageVisibilityState::kHidden: + case content::PageVisibilityState::kHiddenButPainting: + return false; + case content::PageVisibilityState::kVisible: + break; + } + + // Use the visible URL since that's the one the user is aware of (and it + // doesn't matter whether the page loaded successfully). + GURL visible_url = active_web_contents->GetVisibleURL(); + + // view-source: pages are considered to be controlled Service Worker clients + // and thus should be considered when checking the visible URL. However, the + // prefix has to be removed before the origins can be compared. + if (visible_url.SchemeIs(content::kViewSourceScheme)) + visible_url = GURL(visible_url.GetContent()); + + return visible_url.DeprecatedGetOriginAsURL() == origin; +} + +void PushMessagingNotificationManager::ProcessSilentPush( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback, + bool silent_push_allowed) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + + // If the origin was allowed to issue a silent push, just return. + if (silent_push_allowed) { + RecordUserVisibleStatus( + blink::mojom::PushUserVisibleStatus::REQUIRED_BUT_NOT_SHOWN_USED_GRACE); + std::move(message_handled_callback) + .Run(/* did_show_generic_notification= */ false); + return; + } + + RecordUserVisibleStatus(blink::mojom::PushUserVisibleStatus:: + REQUIRED_BUT_NOT_SHOWN_GRACE_EXCEEDED); + + // The site failed to show a notification when one was needed, and they don't + // have enough budget to cover the cost of suppressing, so we will show a + // generic notification. + NotificationDatabaseData database_data = + CreateDatabaseData(origin, service_worker_registration_id); + scoped_refptr<PlatformNotificationContext> notification_context = + GetStoragePartition(profile_, origin)->GetPlatformNotificationContext(); + int64_t next_persistent_notification_id = + PlatformNotificationServiceFactory::GetForProfile(profile_) + ->ReadNextPersistentNotificationId(); + + notification_context->WriteNotificationData( + next_persistent_notification_id, service_worker_registration_id, origin, + database_data, + base::BindOnce( + &PushMessagingNotificationManager::DidWriteNotificationData, + weak_factory_.GetWeakPtr(), std::move(message_handled_callback))); +} + +void PushMessagingNotificationManager::DidWriteNotificationData( + EnforceRequirementsCallback message_handled_callback, + bool success, + const std::string& notification_id) { + DCHECK_CURRENTLY_ON(BrowserThread::UI); + if (!success) + DLOG(ERROR) << "Writing forced notification to database should not fail"; + + std::move(message_handled_callback) + .Run(/* did_show_generic_notification= */ true); +} + +#if BUILDFLAG(IS_CHROMEOS_ASH) +bool PushMessagingNotificationManager::ShouldSkipUserVisibleOnlyRequirements( + const GURL& origin) { + // This is a short-term exception to user visible only enforcement added + // to support for "Messages for Web" integration on ChromeOS. + + chromeos::multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client; + if (test_multidevice_setup_client_) { + multidevice_setup_client = test_multidevice_setup_client_; + } else { + multidevice_setup_client = + ash::multidevice_setup::MultiDeviceSetupClientFactory::GetForProfile( + profile_); + } + + if (!multidevice_setup_client) + return false; + + // Check if messages feature is enabled + if (multidevice_setup_client->GetFeatureState( + chromeos::multidevice_setup::mojom::Feature::kMessages) != + chromeos::multidevice_setup::mojom::FeatureState::kEnabledByUser) { + return false; + } + + ash::android_sms::AndroidSmsAppManager* android_sms_app_manager; + if (test_android_sms_app_manager_) { + android_sms_app_manager = test_android_sms_app_manager_; + } else { + auto* android_sms_service = + ash::android_sms::AndroidSmsServiceFactory::GetForBrowserContext( + profile_); + if (!android_sms_service) + return false; + android_sms_app_manager = android_sms_service->android_sms_app_manager(); + } + + // Check if origin matches current messages url + absl::optional<GURL> app_url = android_sms_app_manager->GetCurrentAppUrl(); + if (!app_url) + app_url = ash::android_sms::GetAndroidMessagesURL(); + + if (!origin.EqualsIgnoringRef(app_url->DeprecatedGetOriginAsURL())) + return false; + + return true; +} + +void PushMessagingNotificationManager::SetTestMultiDeviceSetupClient( + chromeos::multidevice_setup::MultiDeviceSetupClient* + multidevice_setup_client) { + test_multidevice_setup_client_ = multidevice_setup_client; +} + +void PushMessagingNotificationManager::SetTestAndroidSmsAppManager( + ash::android_sms::AndroidSmsAppManager* android_sms_app_manager) { + test_android_sms_app_manager_ = android_sms_app_manager; +} +#endif diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h new file mode 100644 index 00000000000..b2621f55c53 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager.h @@ -0,0 +1,122 @@ +// 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_ + +#include <stdint.h> +#include <vector> + +#include "base/callback_forward.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/push_messaging/budget_database.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/android_sms/android_sms_app_manager.h" +#include "chromeos/services/multidevice_setup/public/cpp/multidevice_setup_client.h" +#endif + +class GURL; +class Profile; + +namespace content { +class WebContents; +} // namespace content + +// Developers may be required to display a Web Notification in response to an +// incoming push message in order to clarify to the user that something has +// happened in the background. When they forget to do so, a default notification +// has to be displayed on their behalf. +// +// This class implements the heuristics for determining whether the default +// notification is necessary, as well as the functionality of displaying the +// default notification when it is. +// +// See the following document and bug for more context: +// https://docs.google.com/document/d/13VxFdLJbMwxHrvnpDm8RXnU41W2ZlcP0mdWWe9zXQT8/edit +// https://crbug.com/437277 +class PushMessagingNotificationManager { + public: + using EnforceRequirementsCallback = + base::OnceCallback<void(bool did_show_generic_notification)>; + + explicit PushMessagingNotificationManager(Profile* profile); + + PushMessagingNotificationManager(const PushMessagingNotificationManager&) = + delete; + PushMessagingNotificationManager& operator=( + const PushMessagingNotificationManager&) = delete; + + ~PushMessagingNotificationManager(); + + // Enforces the requirements implied for push subscriptions which must display + // a Web Notification in response to an incoming message. + void EnforceUserVisibleOnlyRequirements( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback); + + private: + FRIEND_TEST_ALL_PREFIXES(PushMessagingNotificationManagerTest, IsTabVisible); + FRIEND_TEST_ALL_PREFIXES(PushMessagingNotificationManagerTest, + IsTabVisibleViewSource); + FRIEND_TEST_ALL_PREFIXES( + PushMessagingNotificationManagerTest, + SkipEnforceUserVisibleOnlyRequirementsForAndroidMessages); + + void DidCountVisibleNotifications( + const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback, + bool success, + int notification_count); + + // Checks whether |profile| is the one owning this instance, + // |active_web_contents| exists and its main frame is visible, and the URL + // currently visible to the user is for |origin|. + bool IsTabVisible(Profile* profile, + content::WebContents* active_web_contents, + const GURL& origin); + + void ProcessSilentPush(const GURL& origin, + int64_t service_worker_registration_id, + EnforceRequirementsCallback message_handled_callback, + bool silent_push_allowed); + + void DidWriteNotificationData( + EnforceRequirementsCallback message_handled_callback, + bool success, + const std::string& notification_id); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + bool ShouldSkipUserVisibleOnlyRequirements(const GURL& origin); + + void SetTestMultiDeviceSetupClient( + chromeos::multidevice_setup::MultiDeviceSetupClient* + multidevice_setup_client); + + void SetTestAndroidSmsAppManager( + ash::android_sms::AndroidSmsAppManager* android_sms_app_manager); +#endif + + // Weak. This manager is owned by a keyed service on this profile. + raw_ptr<Profile> profile_; + + BudgetDatabase budget_database_; + +#if BUILDFLAG(IS_CHROMEOS_ASH) + chromeos::multidevice_setup::MultiDeviceSetupClient* + test_multidevice_setup_client_ = nullptr; + + ash::android_sms::AndroidSmsAppManager* test_android_sms_app_manager_ = + nullptr; +#endif + + base::WeakPtrFactory<PushMessagingNotificationManager> weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_NOTIFICATION_MANAGER_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc new file mode 100644 index 00000000000..7b6b46680f5 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_notification_manager_unittest.cc @@ -0,0 +1,87 @@ +// 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. + +#include "chrome/browser/push_messaging/push_messaging_notification_manager.h" + +#include "base/bind.h" +#include "build/chromeos_buildflags.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/test_renderer_host.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include <memory> + +#include "chrome/browser/ash/android_sms/fake_android_sms_app_manager.h" +#include "chromeos/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h" +#endif + +class PushMessagingNotificationManagerTest + : public ChromeRenderViewHostTestHarness {}; + +TEST_F(PushMessagingNotificationManagerTest, IsTabVisible) { + PushMessagingNotificationManager manager(profile()); + GURL origin("https://google.com/"); + GURL origin_with_path = origin.Resolve("/path/"); + NavigateAndCommit(origin_with_path); + + EXPECT_FALSE(manager.IsTabVisible(profile(), nullptr, origin)); + EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), + GURL("https://chrome.com/"))); + EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin)); + + content::RenderViewHostTester::For(rvh())->SimulateWasHidden(); + EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), origin)); + + content::RenderViewHostTester::For(rvh())->SimulateWasShown(); + EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin)); +} + +TEST_F(PushMessagingNotificationManagerTest, IsTabVisibleViewSource) { + PushMessagingNotificationManager manager(profile()); + + GURL origin("https://google.com/"); + GURL view_source_page("view-source:https://google.com/path/"); + + NavigateAndCommit(view_source_page); + + ASSERT_EQ(view_source_page, web_contents()->GetVisibleURL()); + EXPECT_TRUE(manager.IsTabVisible(profile(), web_contents(), origin)); + + content::RenderViewHostTester::For(rvh())->SimulateWasHidden(); + EXPECT_FALSE(manager.IsTabVisible(profile(), web_contents(), origin)); +} + +#if BUILDFLAG(IS_CHROMEOS_ASH) +TEST_F(PushMessagingNotificationManagerTest, + SkipEnforceUserVisibleOnlyRequirementsForAndroidMessages) { + GURL app_url("https://example.com/test/"); + auto fake_android_sms_app_manager = + std::make_unique<ash::android_sms::FakeAndroidSmsAppManager>(); + fake_android_sms_app_manager->SetInstalledAppUrl(app_url); + + auto fake_multidevice_setup_client = std::make_unique< + chromeos::multidevice_setup::FakeMultiDeviceSetupClient>(); + fake_multidevice_setup_client->SetFeatureState( + chromeos::multidevice_setup::mojom::Feature::kMessages, + chromeos::multidevice_setup::mojom::FeatureState::kEnabledByUser); + + PushMessagingNotificationManager manager(profile()); + manager.SetTestMultiDeviceSetupClient(fake_multidevice_setup_client.get()); + manager.SetTestAndroidSmsAppManager(fake_android_sms_app_manager.get()); + + bool was_called = false; + manager.EnforceUserVisibleOnlyRequirements( + app_url.DeprecatedGetOriginAsURL(), 0l, + base::BindOnce( + [](bool* was_called, bool did_show_generic_notification) { + *was_called = true; + }, + &was_called)); + EXPECT_TRUE(was_called); +} +#endif diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc b/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc new file mode 100644 index 00000000000..2f8bdee4b43 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher.cc @@ -0,0 +1,121 @@ +// 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. + +#include "chrome/browser/push_messaging/push_messaging_refresher.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/feature_list.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" +#include "chrome/browser/push_messaging/push_messaging_utils.h" +#include "chrome/common/pref_names.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/push_messaging_service.h" +#include "content/public/common/content_features.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" +#include "url/gurl.h" + +PushMessagingRefresher::PushMessagingRefresher() = default; + +PushMessagingRefresher::~PushMessagingRefresher() = default; + +size_t PushMessagingRefresher::GetCount() const { + return old_subscriptions_.size(); +} + +void PushMessagingRefresher::Refresh( + PushMessagingAppIdentifier old_app_identifier, + const std::string& new_app_id, + const std::string& sender_id) { + RefreshObject refresh_object = {old_app_identifier, sender_id, + false /* is_valid */}; + // Insert is as current started refresh + old_subscriptions_.emplace(new_app_id, refresh_object); + refresh_map_.emplace(old_app_identifier.app_id(), new_app_id); + // TODO(viviy): Save old_subscription in a seperate map in preferences, so + // that in case of a browser shutdown the subscription is remembered. + // Unsubscribe on next startup. +} + +void PushMessagingRefresher::OnSubscriptionUpdated( + const std::string& new_app_id) { + RefreshInfo::iterator result = old_subscriptions_.find(new_app_id); + + if (result == old_subscriptions_.end()) + return; + + // Schedule a unsubscription event for the old subscription + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&PushMessagingRefresher::NotifyOnOldSubscriptionExpired, + weak_factory_.GetWeakPtr(), + result->second.old_identifier.app_id(), + result->second.sender_id), + kPushSubscriptionRefreshTimeDelta); +} + +void PushMessagingRefresher::NotifyOnOldSubscriptionExpired( + const std::string& old_app_id, + const std::string& sender_id) { + for (Observer& obs : observers_) + obs.OnOldSubscriptionExpired(old_app_id, sender_id); +} + +void PushMessagingRefresher::OnUnsubscribed(const std::string& old_app_id) { + auto found_new_app_id = refresh_map_.find(old_app_id); + // Already unsubscribed + if (found_new_app_id == refresh_map_.end()) + return; + + std::string new_app_id = found_new_app_id->second; + refresh_map_.erase(found_new_app_id); + + RefreshInfo::iterator result = old_subscriptions_.find(new_app_id); + DCHECK(result != old_subscriptions_.end()); + + PushMessagingAppIdentifier old_identifier = result->second.old_identifier; + old_subscriptions_.erase(result); + + for (Observer& obs : observers_) + obs.OnRefreshFinished(old_identifier); +} + +void PushMessagingRefresher::GotMessageFrom(const std::string& app_id) { + RefreshInfo::iterator result = old_subscriptions_.find(app_id); + // If a message arrives that is part of the refresh, expire the old + // subscription immediately + if (result != old_subscriptions_.end() && !result->second.is_valid) { + NotifyOnOldSubscriptionExpired(result->second.old_identifier.app_id(), + result->second.sender_id); + result->second.is_valid = true; + } +} + +absl::optional<PushMessagingAppIdentifier> +PushMessagingRefresher::FindActiveAppIdentifier(const std::string& app_id) { + absl::optional<PushMessagingAppIdentifier> app_identifier; + RefreshMap::iterator refresh_map_it = refresh_map_.find(app_id); + if (refresh_map_it != refresh_map_.end()) { + RefreshInfo::iterator result = + old_subscriptions_.find(refresh_map_it->second); + if (result != old_subscriptions_.end() && !result->second.is_valid) { + app_identifier = result->second.old_identifier; + } + } + return app_identifier; +} + +base::WeakPtr<PushMessagingRefresher> PushMessagingRefresher::GetWeakPtr() { + return weak_factory_.GetWeakPtr(); +} + +void PushMessagingRefresher::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void PushMessagingRefresher::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher.h b/chromium/chrome/browser/push_messaging/push_messaging_refresher.h new file mode 100644 index 00000000000..c049a14388e --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher.h @@ -0,0 +1,99 @@ +// 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_ + +#include <map> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/observer_list_types.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "content/public/browser/push_messaging_service.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom-forward.h" + +// This class enables push subscription refreshes as defined in the docs: +// https://w3c.github.io/push-api/#subscription-refreshes +// The idea is to keep the refresh information of both new and old subscription +// in memory during the refresh process to be still able to receive messages +// through the old subscription after it was replaced by the new subscription. +class PushMessagingRefresher { + public: + PushMessagingRefresher(); + + PushMessagingRefresher(const PushMessagingRefresher&) = delete; + PushMessagingRefresher& operator=(const PushMessagingRefresher&) = delete; + + ~PushMessagingRefresher(); + + // Return number of objects that are currently being refreshed + size_t GetCount() const; + + // Register a new refresh pair with relevant information. + void Refresh(PushMessagingAppIdentifier old_app_identifier, + const std::string& new_app_id, + const std::string& sender_id); + + // The subscription with the new app id was updated, new messages arriving + // through the new subscription should be accepted now. + void OnSubscriptionUpdated(const std::string& new_app_id); + + // Unsubscribe event happened for the old subscription. It is deleted in + // the RefreshMap and notify all observers that the refresh process for + // |app_id| has finished + void OnUnsubscribed(const std::string& app_id); + + // If a new message arrives through an |app_id| that is associated with a + // refresh, the old subscription needs to be deactivated. + void GotMessageFrom(const std::string& app_id); + + // If a subscription was refreshed, we accept the old subscription for + // a moment after refresh + absl::optional<PushMessagingAppIdentifier> FindActiveAppIdentifier( + const std::string& app_id); + + base::WeakPtr<PushMessagingRefresher> GetWeakPtr(); + + // Observer for Refresh status updates + class Observer : public base::CheckedObserver { + public: + virtual void OnOldSubscriptionExpired(const std::string& app_id, + const std::string& sender_id) = 0; + virtual void OnRefreshFinished( + const PushMessagingAppIdentifier& app_identifier) = 0; + }; + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + private: + // A RefreshObject carries subscription information that is needed to receive + // messages and to unsubscribe from the old subscription + struct RefreshObject { + PushMessagingAppIdentifier old_identifier; + std::string sender_id; + bool is_valid; + }; + + void NotifyOnOldSubscriptionExpired(const std::string& app_id, + const std::string& sender_id); + + base::ObserverList<Observer> observers_; + + // Maps from new app id to the refresh information of the old subscription + // that is needed to receive messages and unsubscribe + using RefreshInfo = std::map<std::string, RefreshObject>; + RefreshInfo old_subscriptions_; + + // Maps from old app id to new app id + using RefreshMap = std::map<std::string, std::string>; + RefreshMap refresh_map_; + + base::WeakPtrFactory<PushMessagingRefresher> weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_REFRESHER_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc new file mode 100644 index 00000000000..63570f41e83 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_refresher_unittest.cc @@ -0,0 +1,84 @@ +// 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. + +#include "chrome/browser/push_messaging/push_messaging_refresher.h" + +#include <stdint.h> + +#include "base/time/time.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_refresher.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/gurl.h" +namespace { + +void ExpectAppIdentifiersEqual(const PushMessagingAppIdentifier& a, + const PushMessagingAppIdentifier& b) { + EXPECT_EQ(a.app_id(), b.app_id()); + EXPECT_EQ(a.origin(), b.origin()); + EXPECT_EQ(a.service_worker_registration_id(), + b.service_worker_registration_id()); + EXPECT_EQ(a.expiration_time(), b.expiration_time()); +} + +constexpr char kTestOrigin[] = "https://example.com"; +constexpr char kTestSenderId[] = "1234567890"; +const int64_t kTestServiceWorkerId = 42; + +class PushMessagingRefresherTest : public testing::Test { + protected: + void SetUp() override { + old_app_identifier_ = PushMessagingAppIdentifier::Generate( + GURL(kTestOrigin), kTestServiceWorkerId); + new_app_identifier_ = PushMessagingAppIdentifier::Generate( + GURL(kTestOrigin), kTestServiceWorkerId); + } + + Profile* profile() { return &profile_; } + + PushMessagingRefresher* refresher() { return &refresher_; } + + absl::optional<PushMessagingAppIdentifier> old_app_identifier_; + absl::optional<PushMessagingAppIdentifier> new_app_identifier_; + + private: + content::BrowserTaskEnvironment task_environment_; + TestingProfile profile_; + PushMessagingRefresher refresher_; +}; + +TEST_F(PushMessagingRefresherTest, GotMessageThroughNewSubscription) { + refresher()->Refresh(old_app_identifier_.value(), + new_app_identifier_.value().app_id(), kTestSenderId); + refresher()->GotMessageFrom(new_app_identifier_.value().app_id()); + auto app_identifier = refresher()->FindActiveAppIdentifier( + old_app_identifier_.value().app_id()); + EXPECT_FALSE(app_identifier.has_value()); +} + +TEST_F(PushMessagingRefresherTest, LookupOldSubscription) { + refresher()->Refresh(old_app_identifier_.value(), + new_app_identifier_.value().app_id(), kTestSenderId); + { + absl::optional<PushMessagingAppIdentifier> found_old_app_identifier = + refresher()->FindActiveAppIdentifier( + old_app_identifier_.value().app_id()); + EXPECT_TRUE(found_old_app_identifier.has_value()); + ExpectAppIdentifiersEqual(old_app_identifier_.value(), + found_old_app_identifier.value()); + } + refresher()->OnUnsubscribed(old_app_identifier_.value().app_id()); + { + absl::optional<PushMessagingAppIdentifier> found_after_unsubscribe = + refresher()->FindActiveAppIdentifier( + old_app_identifier_.value().app_id()); + EXPECT_FALSE(found_after_unsubscribe.has_value()); + } + EXPECT_EQ(0u, refresher()->GetCount()); +} + +} // namespace diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc new file mode 100644 index 00000000000..2987c24efdf --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.cc @@ -0,0 +1,82 @@ +// 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. + +#include "chrome/browser/push_messaging/push_messaging_service_factory.h" + +#include <memory> + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/engagement/site_engagement_service_factory.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" +#include "chrome/browser/permissions/permission_manager_factory.h" +#include "chrome/browser/profiles/incognito_helpers.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/android_sms/android_sms_service_factory.h" +#include "chrome/browser/ash/multidevice_setup/multidevice_setup_client_factory.h" +#endif + +// static +PushMessagingServiceImpl* PushMessagingServiceFactory::GetForProfile( + content::BrowserContext* context) { + // The Push API is not currently supported in incognito mode. + // See https://crbug.com/401439. + if (context->IsOffTheRecord()) + return nullptr; + + return static_cast<PushMessagingServiceImpl*>( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +// static +PushMessagingServiceFactory* PushMessagingServiceFactory::GetInstance() { + return base::Singleton<PushMessagingServiceFactory>::get(); +} + +PushMessagingServiceFactory::PushMessagingServiceFactory() + : BrowserContextKeyedServiceFactory( + "PushMessagingProfileService", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(gcm::GCMProfileServiceFactory::GetInstance()); + DependsOn(instance_id::InstanceIDProfileServiceFactory::GetInstance()); + DependsOn(HostContentSettingsMapFactory::GetInstance()); + DependsOn(PermissionManagerFactory::GetInstance()); + DependsOn(site_engagement::SiteEngagementServiceFactory::GetInstance()); +#if BUILDFLAG(IS_CHROMEOS_ASH) + DependsOn(ash::android_sms::AndroidSmsServiceFactory::GetInstance()); + DependsOn( + ash::multidevice_setup::MultiDeviceSetupClientFactory::GetInstance()); +#endif +} + +PushMessagingServiceFactory::~PushMessagingServiceFactory() {} + +void PushMessagingServiceFactory::RestoreFactoryForTests( + content::BrowserContext* context) { + SetTestingFactory(context, + base::BindRepeating([](content::BrowserContext* context) { + return base::WrapUnique( + GetInstance()->BuildServiceInstanceFor(context)); + })); +} + +KeyedService* PushMessagingServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + CHECK(!profile->IsOffTheRecord()); + return new PushMessagingServiceImpl(profile); +} + +content::BrowserContext* PushMessagingServiceFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return chrome::GetBrowserContextOwnInstanceInIncognito(context); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h new file mode 100644 index 00000000000..5512ed8dfe7 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_factory.h @@ -0,0 +1,40 @@ +// 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class PushMessagingServiceImpl; + +class PushMessagingServiceFactory : public BrowserContextKeyedServiceFactory { + public: + static PushMessagingServiceImpl* GetForProfile( + content::BrowserContext* profile); + static PushMessagingServiceFactory* GetInstance(); + + PushMessagingServiceFactory(const PushMessagingServiceFactory&) = delete; + PushMessagingServiceFactory& operator=(const PushMessagingServiceFactory&) = + delete; + + // Undo a previous call to SetTestingFactory, such that subsequent calls to + // GetForProfile get a real push service. + void RestoreFactoryForTests(content::BrowserContext* context); + + private: + friend struct base::DefaultSingletonTraits<PushMessagingServiceFactory>; + + PushMessagingServiceFactory(); + ~PushMessagingServiceFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_FACTORY_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc new file mode 100644 index 00000000000..6313d399760 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.cc @@ -0,0 +1,1684 @@ +// Copyright 2014 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. + +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" + +#include <map> +#include <sstream> +#include <vector> + +#include "base/barrier_closure.h" +#include "base/base64url.h" +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/command_line.h" +#include "base/feature_list.h" +#include "base/logging.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_util.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/gcm/instance_id/instance_id_profile_service_factory.h" +#include "chrome/browser/permissions/abusive_origin_permission_revocation_request.h" +#include "chrome/browser/permissions/permission_manager_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_keep_alive_types.h" +#include "chrome/browser/profiles/scoped_profile_keep_alive.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "chrome/browser/push_messaging/push_messaging_features.h" +#include "chrome/browser/push_messaging/push_messaging_service_factory.h" +#include "chrome/browser/push_messaging/push_messaging_utils.h" +#include "chrome/browser/ui/chrome_pages.h" +#include "chrome/common/buildflags.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "chrome/grit/generated_resources.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/gcm_driver/instance_id/instance_id.h" +#include "components/gcm_driver/instance_id/instance_id_driver.h" +#include "components/gcm_driver/instance_id/instance_id_profile_service.h" +#include "components/permissions/permission_manager.h" +#include "components/permissions/permission_result.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/devtools_background_services_context.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/service_worker_context.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/common/child_process_host.h" +#include "content/public/common/content_features.h" +#include "content/public/common/content_switches.h" +#include "third_party/blink/public/mojom/devtools/console_message.mojom.h" +#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" +#include "ui/base/l10n/l10n_util.h" + +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) +#include "chrome/browser/background/background_mode_manager.h" +#include "components/keep_alive_registry/keep_alive_types.h" +#include "components/keep_alive_registry/scoped_keep_alive.h" +#endif + +#if defined(OS_ANDROID) +#include "base/android/jni_android.h" +#include "chrome/android/chrome_jni_headers/PushMessagingServiceObserver_jni.h" +#endif + +using instance_id::InstanceID; + +namespace { + +// Scope passed to getToken to obtain GCM registration tokens. +// Must match Java GoogleCloudMessaging.INSTANCE_ID_SCOPE. +const char kGCMScope[] = "GCM"; + +const int kMaxRegistrations = 1000000; + +// Chrome does not yet support silent push messages, and requires websites to +// indicate that they will only send user-visible messages. +const char kSilentPushUnsupportedMessage[] = + "Chrome currently only supports the Push API for subscriptions that will " + "result in user-visible messages. You can indicate this by calling " + "pushManager.subscribe({userVisibleOnly: true}) instead. See " + "https://goo.gl/yqv4Q4 for more details."; + +// Message displayed in the console (as an error) when a GCM Sender ID is used +// to create a subscription, which is unsupported. The subscription request will +// have been blocked, and an exception will be thrown as well. +const char kSenderIdRegistrationDisallowedMessage[] = + "The provided application server key is not a VAPID key. Only VAPID keys " + "are supported. For more information check https://crbug.com/979235."; + +// Message displayed in the console (as a warning) when a GCM Sender ID is used +// to create a subscription, which will soon be unsupported. +const char kSenderIdRegistrationDeprecatedMessage[] = + "The provided application server key is not a VAPID key. Only VAPID keys " + "will be supported in the future. For more information check " + "https://crbug.com/979235."; + +void RecordDeliveryStatus(blink::mojom::PushEventStatus status) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.DeliveryStatus", status); +} + +void RecordPushSubcriptionChangeStatus(blink::mojom::PushEventStatus status) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.PushSubscriptionChangeStatus", + status); +} +void RecordUnsubscribeReason(blink::mojom::PushUnregistrationReason reason) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationReason", reason); +} + +void RecordUnsubscribeGCMResult(gcm::GCMClient::Result result) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationGCMResult", result); +} + +void RecordUnsubscribeIIDResult(InstanceID::Result result) { + UMA_HISTOGRAM_ENUMERATION("PushMessaging.UnregistrationIIDResult", result); +} + +blink::mojom::PermissionStatus ToPermissionStatus( + ContentSetting content_setting) { + switch (content_setting) { + case CONTENT_SETTING_ALLOW: + return blink::mojom::PermissionStatus::GRANTED; + case CONTENT_SETTING_BLOCK: + return blink::mojom::PermissionStatus::DENIED; + case CONTENT_SETTING_ASK: + return blink::mojom::PermissionStatus::ASK; + default: + break; + } + NOTREACHED(); + return blink::mojom::PermissionStatus::DENIED; +} + +void UnregisterCallbackToClosure( + base::OnceClosure closure, + blink::mojom::PushUnregistrationStatus status) { + DCHECK(closure); + std::move(closure).Run(); +} + +void LogMessageReceivedEventToDevTools( + content::DevToolsBackgroundServicesContext* devtools_context, + const PushMessagingAppIdentifier& app_identifier, + const std::string& message_id, + bool was_encrypted, + const std::string& error_message, + const std::string& payload) { + if (!devtools_context) + return; + + std::map<std::string, std::string> event_metadata = { + {"Success", error_message.empty() ? "Yes" : "No"}, + {"Was Encrypted", was_encrypted ? "Yes" : "No"}}; + + if (!error_message.empty()) + event_metadata["Error Reason"] = error_message; + else if (was_encrypted) + event_metadata["Payload"] = payload; + + devtools_context->LogBackgroundServiceEvent( + app_identifier.service_worker_registration_id(), + url::Origin::Create(app_identifier.origin()), + content::DevToolsBackgroundService::kPushMessaging, + "Push message received" /* event_name */, message_id, event_metadata); +} + +PendingMessage::PendingMessage(std::string app_id, gcm::IncomingMessage message) + : app_id(std::move(app_id)), + message(std::move(message)), + received_time(base::Time::Now()) {} +PendingMessage::PendingMessage(const PendingMessage& other) = default; +PendingMessage::PendingMessage(PendingMessage&& other) = default; +PendingMessage& PendingMessage::operator=(PendingMessage&& other) = default; +PendingMessage::~PendingMessage() = default; + +} // namespace + +// static +void PushMessagingServiceImpl::InitializeForProfile(Profile* profile) { + // TODO(johnme): Consider whether push should be enabled in incognito. + if (!profile || profile->IsOffTheRecord()) + return; + + int count = PushMessagingAppIdentifier::GetCount(profile); + if (count <= 0) + return; + + PushMessagingServiceImpl* push_service = + PushMessagingServiceFactory::GetForProfile(profile); + if (push_service) { + push_service->IncreasePushSubscriptionCount(count, false /* is_pending */); + push_service->RemoveExpiredSubscriptions(); + } +} + +void PushMessagingServiceImpl::RemoveExpiredSubscriptions() { + if (!base::FeatureList::IsEnabled( + features::kPushSubscriptionWithExpirationTime)) { + return; + } + + base::RepeatingClosure barrier_closure = base::BarrierClosure( + PushMessagingAppIdentifier::GetCount(profile_), + remove_expired_subscriptions_callback_for_testing_.is_null() + ? base::DoNothing() + : std::move(remove_expired_subscriptions_callback_for_testing_)); + + for (const auto& identifier : PushMessagingAppIdentifier::GetAll(profile_)) { + if (!identifier.IsExpired()) { + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, barrier_closure); + continue; + } + content::BrowserThread::PostBestEffortTask( + FROM_HERE, base::ThreadTaskRunnerHandle::Get(), + base::BindOnce( + &PushMessagingServiceImpl::UnexpectedChange, + weak_factory_.GetWeakPtr(), identifier, + blink::mojom::PushUnregistrationReason::SUBSCRIPTION_EXPIRED, + barrier_closure)); + } +} + +void PushMessagingServiceImpl::UnexpectedChange( + PushMessagingAppIdentifier identifier, + blink::mojom::PushUnregistrationReason reason, + base::OnceClosure completed_closure) { + auto unsubscribe_closure = + base::BindOnce(&PushMessagingServiceImpl::UnexpectedUnsubscribe, + weak_factory_.GetWeakPtr(), identifier, reason, + base::BindOnce(&UnregisterCallbackToClosure, + std::move(completed_closure))); + if (base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent)) { + // Find old subscription and fire a `pushsubscriptionchange` event + GetPushSubscriptionFromAppIdentifier( + identifier, + base::BindOnce(&PushMessagingServiceImpl::FirePushSubscriptionChange, + weak_factory_.GetWeakPtr(), identifier, + std::move(unsubscribe_closure), + nullptr /* new_subscription */)); + } else { + std::move(unsubscribe_closure).Run(); + } +} + +PushMessagingServiceImpl::PushMessagingServiceImpl(Profile* profile) + : profile_(profile), + push_subscription_count_(0), + pending_push_subscription_count_(0), + notification_manager_(profile) { + DCHECK(profile); + HostContentSettingsMapFactory::GetForProfile(profile_)->AddObserver(this); + + registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING, + content::NotificationService::AllSources()); + refresh_observation_.Observe(&refresher_); +} + +PushMessagingServiceImpl::~PushMessagingServiceImpl() = default; + +void PushMessagingServiceImpl::IncreasePushSubscriptionCount(int add, + bool is_pending) { + DCHECK_GT(add, 0); + if (push_subscription_count_ + pending_push_subscription_count_ == 0) + GetGCMDriver()->AddAppHandler(kPushMessagingAppIdentifierPrefix, this); + + if (is_pending) + pending_push_subscription_count_ += add; + else + push_subscription_count_ += add; +} + +void PushMessagingServiceImpl::DecreasePushSubscriptionCount(int subtract, + bool was_pending) { + DCHECK_GT(subtract, 0); + if (was_pending) { + pending_push_subscription_count_ -= subtract; + DCHECK_GE(pending_push_subscription_count_, 0); + } else { + push_subscription_count_ -= subtract; + DCHECK_GE(push_subscription_count_, 0); + } + + if (push_subscription_count_ + pending_push_subscription_count_ == 0) + GetGCMDriver()->RemoveAppHandler(kPushMessagingAppIdentifierPrefix); +} + +bool PushMessagingServiceImpl::CanHandle(const std::string& app_id) const { + return base::StartsWith(app_id, kPushMessagingAppIdentifierPrefix, + base::CompareCase::INSENSITIVE_ASCII); +} + +void PushMessagingServiceImpl::ShutdownHandler() { + // Shutdown() should come before and it removes us from the list of app + // handlers of gcm::GCMDriver so this shouldn't ever been called. + NOTREACHED(); +} + +void PushMessagingServiceImpl::OnStoreReset() { + // Delete all cached subscriptions, since they are now invalid. + for (const auto& identifier : PushMessagingAppIdentifier::GetAll(profile_)) { + RecordUnsubscribeReason( + blink::mojom::PushUnregistrationReason::GCM_STORE_RESET); + // Clear all the subscriptions in parallel, to reduce risk that shutdown + // occurs before we finish clearing them. + ClearPushSubscriptionId(profile_, identifier.origin(), + identifier.service_worker_registration_id(), + base::DoNothing()); + // TODO(johnme): Fire pushsubscriptionchange/pushsubscriptionlost SW event. + } + PushMessagingAppIdentifier::DeleteAllFromPrefs(profile_); +} + +// OnMessage methods ----------------------------------------------------------- + +void PushMessagingServiceImpl::OnMessage(const std::string& app_id, + const gcm::IncomingMessage& message) { + // We won't have time to process and act on the message. + // TODO(peter) This should be checked at the level of the GCMDriver, so that + // the message is not consumed. See https://crbug.com/612815 + if (g_browser_process->IsShuttingDown() || shutdown_started_) + return; + +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + if (g_browser_process->background_mode_manager()) { + UMA_HISTOGRAM_BOOLEAN("PushMessaging.ReceivedMessageInBackground", + g_browser_process->background_mode_manager() + ->IsBackgroundWithoutWindows()); + } + + if (!in_flight_keep_alive_) { + in_flight_keep_alive_ = std::make_unique<ScopedKeepAlive>( + KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE, + KeepAliveRestartOption::DISABLED); + in_flight_profile_keep_alive_ = std::make_unique<ScopedProfileKeepAlive>( + profile_, ProfileKeepAliveOrigin::kInFlightPushMessage); + } +#endif + + refresher_.GotMessageFrom(app_id); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + // Drop message and unregister if app_id was unknown (maybe recently deleted). + if (app_identifier.is_null()) { + absl::optional<PushMessagingAppIdentifier> refresh_identifier = + refresher_.FindActiveAppIdentifier(app_id); + if (!refresh_identifier) { + DeliverMessageCallback(app_id, GURL::EmptyGURL(), + -1 /* kInvalidServiceWorkerRegistrationId */, + message, false /* did_enqueue_message */, + blink::mojom::PushEventStatus::UNKNOWN_APP_ID); + return; + } + app_identifier = std::move(*refresh_identifier); + } + + LogMessageReceivedEventToDevTools( + GetDevToolsContext(app_identifier.origin()), app_identifier, + message.message_id, + /* was_encrypted= */ message.decrypted, std::string() /* error_message */, + message.decrypted ? message.raw_data : std::string()); + + if (IsPermissionSet(app_identifier.origin())) { + messages_pending_permission_check_.emplace(app_id, message); + // Start abusive origin verification only if no other verification is in + // progress. + if (!abusive_origin_revocation_request_) + CheckOriginForAbuseAndDispatchNextMessage(); + } else { + // Drop message and unregister if origin has lost push permission. + DeliverMessageCallback(app_id, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + message, false /* did_enqueue_message */, + blink::mojom::PushEventStatus::PERMISSION_DENIED); + } +} + +void PushMessagingServiceImpl::CheckOriginForAbuseAndDispatchNextMessage() { + if (messages_pending_permission_check_.empty()) + return; + + PendingMessage message = + std::move(messages_pending_permission_check_.front()); + messages_pending_permission_check_.pop(); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, message.app_id); + + if (app_identifier.is_null()) { + CheckOriginForAbuseAndDispatchNextMessage(); + return; + } + + DCHECK(!abusive_origin_revocation_request_) + << "Create one Abusive Origin Revocation instance per request."; + abusive_origin_revocation_request_ = + std::make_unique<AbusiveOriginPermissionRevocationRequest>( + profile_, app_identifier.origin(), + base::BindOnce(&PushMessagingServiceImpl::OnCheckedOriginForAbuse, + weak_factory_.GetWeakPtr(), std::move(message))); +} + +void PushMessagingServiceImpl::OnCheckedOriginForAbuse( + PendingMessage message, + AbusiveOriginPermissionRevocationRequest::Outcome outcome) { + abusive_origin_revocation_request_.reset(); + + base::UmaHistogramLongTimes("PushMessaging.CheckOriginForAbuseTime", + base::Time::Now() - message.received_time); + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, message.app_id); + + if (app_identifier.is_null()) { + CheckOriginForAbuseAndDispatchNextMessage(); + return; + } + + const GURL& origin = app_identifier.origin(); + int64_t service_worker_registration_id = + app_identifier.service_worker_registration_id(); + + // It is possible that Notifications permission has been revoked by an user + // during abusive origin verification. + if (outcome == AbusiveOriginPermissionRevocationRequest::Outcome:: + PERMISSION_NOT_REVOKED && + IsPermissionSet(origin)) { + std::queue<PendingMessage>& delivery_queue = + message_delivery_queue_[{origin, service_worker_registration_id}]; + delivery_queue.push(std::move(message)); + + // Start delivering push messages to this service worker if this was the + // first message. Otherwise just enqueue the message to be delivered once + // all previous messages have been handled. + if (delivery_queue.size() == 1) { + DeliverNextQueuedMessageForServiceWorkerRegistration( + origin, service_worker_registration_id); + } + } else { + // Drop message and unregister if origin has lost push permission. + DeliverMessageCallback( + message.app_id, origin, service_worker_registration_id, message.message, + false /* did_enqueue_message */, + outcome == AbusiveOriginPermissionRevocationRequest::Outcome:: + PERMISSION_NOT_REVOKED + ? blink::mojom::PushEventStatus::PERMISSION_DENIED + : blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE); + } + + // Verify the next message in the queue. + CheckOriginForAbuseAndDispatchNextMessage(); +} + +void PushMessagingServiceImpl:: + DeliverNextQueuedMessageForServiceWorkerRegistration( + const GURL& origin, + int64_t service_worker_registration_id) { + MessageDeliveryQueueKey key{origin, service_worker_registration_id}; + auto iter = message_delivery_queue_.find(key); + if (iter == message_delivery_queue_.end()) + return; + + const std::queue<PendingMessage>& delivery_queue = iter->second; + CHECK(!delivery_queue.empty()); + const PendingMessage& next_message = delivery_queue.front(); + + const std::string& app_id = next_message.app_id; + const gcm::IncomingMessage& message = next_message.message; + + auto deliver_message_callback = base::BindOnce( + &PushMessagingServiceImpl::DeliverMessageCallback, + weak_factory_.GetWeakPtr(), app_id, origin, + service_worker_registration_id, message, true /* did_enqueue_message */); + + // It is possible that Notification permissions have been revoked by a user + // while handling previous messages for |origin|. + if (!IsPermissionSet(origin)) { + std::move(deliver_message_callback) + .Run(blink::mojom::PushEventStatus::PERMISSION_DENIED); + return; + } + + // The payload of a push message can be valid with content, valid with empty + // content, or null. + absl::optional<std::string> payload; + if (message.decrypted) + payload = message.raw_data; + + base::UmaHistogramLongTimes("PushMessaging.DeliverQueuedMessageTime", + base::Time::Now() - next_message.received_time); + + // Inform tests observing message dispatching about the event. + if (message_dispatched_callback_for_testing_) { + message_dispatched_callback_for_testing_.Run( + app_id, origin, service_worker_registration_id, std::move(payload), + std::move(deliver_message_callback)); + return; + } + + // Dispatch the message to the appropriate Service Worker. + profile_->DeliverPushMessage(origin, service_worker_registration_id, + message.message_id, payload, + std::move(deliver_message_callback)); +} + +void PushMessagingServiceImpl::DeliverMessageCallback( + const std::string& app_id, + const GURL& requesting_origin, + int64_t service_worker_registration_id, + const gcm::IncomingMessage& message, + bool did_enqueue_message, + blink::mojom::PushEventStatus status) { + RecordDeliveryStatus(status); + + // Note: It's important that |message_handled_callback| is run or passed to + // another function before this function returns. + auto message_handled_callback = + base::BindOnce(&PushMessagingServiceImpl::DidHandleMessage, + weak_factory_.GetWeakPtr(), app_id, message.message_id); + + if (did_enqueue_message) { + message_handled_callback = base::BindOnce( + &PushMessagingServiceImpl::DidHandleEnqueuedMessage, + weak_factory_.GetWeakPtr(), requesting_origin, + service_worker_registration_id, std::move(message_handled_callback)); + } + + // A reason to automatically unsubscribe. UNKNOWN means do not unsubscribe. + blink::mojom::PushUnregistrationReason unsubscribe_reason = + blink::mojom::PushUnregistrationReason::UNKNOWN; + + // TODO(mvanouwerkerk): Show a warning in the developer console of the + // Service Worker corresponding to app_id (and/or on an internals page). + // See https://crbug.com/508516 for options. + switch (status) { + // Call EnforceUserVisibleOnlyRequirements if the message was delivered to + // the Service Worker JavaScript, even if the website's event handler failed + // (to prevent sites deliberately failing in order to avoid having to show + // notifications). + case blink::mojom::PushEventStatus::SUCCESS: + case blink::mojom::PushEventStatus::EVENT_WAITUNTIL_REJECTED: + case blink::mojom::PushEventStatus::TIMEOUT: + // Only enforce the user visible requirements if silent push has not been + // enabled through a command line flag. + if (!base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kAllowSilentPush)) { + notification_manager_.EnforceUserVisibleOnlyRequirements( + requesting_origin, service_worker_registration_id, + std::move(message_handled_callback)); + message_handled_callback = base::OnceCallback<void(bool)>(); + } + break; + case blink::mojom::PushEventStatus::SERVICE_WORKER_ERROR: + // Do nothing, and hope the error is transient. + break; + case blink::mojom::PushEventStatus::UNKNOWN_APP_ID: + unsubscribe_reason = + blink::mojom::PushUnregistrationReason::DELIVERY_UNKNOWN_APP_ID; + break; + case blink::mojom::PushEventStatus::PERMISSION_DENIED: + unsubscribe_reason = + blink::mojom::PushUnregistrationReason::DELIVERY_PERMISSION_DENIED; + break; + case blink::mojom::PushEventStatus::NO_SERVICE_WORKER: + unsubscribe_reason = + blink::mojom::PushUnregistrationReason::DELIVERY_NO_SERVICE_WORKER; + break; + case blink::mojom::PushEventStatus::PERMISSION_REVOKED_ABUSIVE: + unsubscribe_reason = + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED_ABUSIVE; + break; + } + + // If |message_handled_callback| was not yet used, make a + // |completion_closure_runner| which should run by default at the end of this + // function, unless it is explicitly passed to another function or disabled. + base::ScopedClosureRunner completion_closure_runner( + message_handled_callback + ? base::BindOnce(std::move(message_handled_callback), + false /* did_show_generic_notification */) + : base::DoNothing()); + + if (unsubscribe_reason != blink::mojom::PushUnregistrationReason::UNKNOWN) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + UnsubscribeInternal( + unsubscribe_reason, + app_identifier.is_null() ? GURL::EmptyGURL() : app_identifier.origin(), + app_identifier.is_null() + ? -1 /* kInvalidServiceWorkerRegistrationId */ + : app_identifier.service_worker_registration_id(), + app_id, message.sender_id, + base::BindOnce(&UnregisterCallbackToClosure, + completion_closure_runner.Release())); + + if (app_identifier.is_null()) + return; + + if (auto* devtools_context = GetDevToolsContext(app_identifier.origin())) { + std::stringstream ss; + ss << unsubscribe_reason; + devtools_context->LogBackgroundServiceEvent( + app_identifier.service_worker_registration_id(), + url::Origin::Create(app_identifier.origin()), + content::DevToolsBackgroundService::kPushMessaging, + "Unsubscribed due to error" /* event_name */, message.message_id, + {{"Reason", ss.str()}}); + } + } +} + +void PushMessagingServiceImpl::DidHandleEnqueuedMessage( + const GURL& origin, + int64_t service_worker_registration_id, + base::OnceCallback<void(bool)> message_handled_callback, + bool did_show_generic_notification) { + // Lookup the message queue for the correct service worker. + MessageDeliveryQueueKey key{origin, service_worker_registration_id}; + auto iter = message_delivery_queue_.find(key); + CHECK(iter != message_delivery_queue_.end()); + + // Remove the delivered message from the queue. + std::queue<PendingMessage>& delivery_queue = iter->second; + CHECK(!delivery_queue.empty()); + + base::UmaHistogramLongTimes( + "PushMessaging.MessageHandledTime", + base::Time::Now() - delivery_queue.front().received_time); + + delivery_queue.pop(); + if (delivery_queue.empty()) + message_delivery_queue_.erase(key); + + // This will call PushMessagingServiceImpl::DidHandleMessage(). + std::move(message_handled_callback).Run(did_show_generic_notification); + + // Deliver next message to this service worker now. We deliver them in series + // so we can check the visibility requirements after each message. + DeliverNextQueuedMessageForServiceWorkerRegistration( + origin, service_worker_registration_id); +} + +void PushMessagingServiceImpl::DidHandleMessage( + const std::string& app_id, + const std::string& push_message_id, + bool did_show_generic_notification) { +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + // Reset before running callbacks below, so tests can verify keep-alive reset. + if (message_delivery_queue_.empty()) { + in_flight_keep_alive_.reset(); + in_flight_profile_keep_alive_.reset(); + } +#endif + + if (message_callback_for_testing_) + message_callback_for_testing_.Run(); + +#if defined(OS_ANDROID) + chrome::android::Java_PushMessagingServiceObserver_onMessageHandled( + base::android::AttachCurrentThread()); +#endif + + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + + if (app_identifier.is_null() || !did_show_generic_notification) + return; + + if (auto* devtools_context = GetDevToolsContext(app_identifier.origin())) { + devtools_context->LogBackgroundServiceEvent( + app_identifier.service_worker_registration_id(), + url::Origin::Create(app_identifier.origin()), + content::DevToolsBackgroundService::kPushMessaging, + "Generic notification shown" /* event_name */, push_message_id, + {} /* event_metadata */); + } +} + +void PushMessagingServiceImpl::SetMessageCallbackForTesting( + const base::RepeatingClosure& callback) { + message_callback_for_testing_ = callback; +} + +// Other gcm::GCMAppHandler methods -------------------------------------------- + +void PushMessagingServiceImpl::OnMessagesDeleted(const std::string& app_id) { + // TODO(mvanouwerkerk): Consider firing an event on the Service Worker + // corresponding to |app_id| to inform the app about deleted messages. +} + +void PushMessagingServiceImpl::OnSendError( + const std::string& app_id, + const gcm::GCMClient::SendErrorDetails& send_error_details) { + NOTREACHED() << "The Push API shouldn't have sent messages upstream"; +} + +void PushMessagingServiceImpl::OnSendAcknowledged( + const std::string& app_id, + const std::string& message_id) { + NOTREACHED() << "The Push API shouldn't have sent messages upstream"; +} + +void PushMessagingServiceImpl::OnMessageDecryptionFailed( + const std::string& app_id, + const std::string& message_id, + const std::string& error_message) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + + if (app_identifier.is_null()) + return; + + LogMessageReceivedEventToDevTools( + GetDevToolsContext(app_identifier.origin()), app_identifier, message_id, + /* was_encrypted= */ true, error_message, "" /* payload */); +} + +// Subscribe and GetPermissionStatus methods ----------------------------------- + +void PushMessagingServiceImpl::SubscribeFromDocument( + const GURL& requesting_origin, + int64_t service_worker_registration_id, + int render_process_id, + int render_frame_id, + blink::mojom::PushSubscriptionOptionsPtr options, + bool user_gesture, + RegisterCallback callback) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, requesting_origin, service_worker_registration_id); + + // If there is no existing app identifier for the given Service Worker, + // generate a new one. This will create a new subscription on the server. + if (app_identifier.is_null()) { + app_identifier = PushMessagingAppIdentifier::Generate( + requesting_origin, service_worker_registration_id); + } + + if (push_subscription_count_ + pending_push_subscription_count_ >= + kMaxRegistrations) { + SubscribeEndWithError(std::move(callback), + blink::mojom::PushRegistrationStatus::LIMIT_REACHED); + return; + } + + content::RenderFrameHost* render_frame_host = + content::RenderFrameHost::FromID(render_process_id, render_frame_id); + + if (!render_frame_host) { + // It is possible for `render_frame_host` to be nullptr here due to a race + // (crbug.com/1057981). + SubscribeEndWithError( + std::move(callback), + blink::mojom::PushRegistrationStatus::RENDERER_SHUTDOWN); + return; + } + + if (!options->user_visible_only) { + render_frame_host->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kError, + kSilentPushUnsupportedMessage); + + SubscribeEndWithError( + std::move(callback), + blink::mojom::PushRegistrationStatus::PERMISSION_DENIED); + return; + } + + // Push does not allow permission requests from iframes. + PermissionManagerFactory::GetForProfile(profile_)->RequestPermission( + ContentSettingsType::NOTIFICATIONS, render_frame_host, requesting_origin, + user_gesture, + base::BindOnce(&PushMessagingServiceImpl::DoSubscribe, + weak_factory_.GetWeakPtr(), std::move(app_identifier), + std::move(options), std::move(callback), render_process_id, + render_frame_id)); +} + +void PushMessagingServiceImpl::SubscribeFromWorker( + const GURL& requesting_origin, + int64_t service_worker_registration_id, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback register_callback) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, requesting_origin, service_worker_registration_id); + + // If there is no existing app identifier for the given Service Worker, + // generate a new one. This will create a new subscription on the server. + if (app_identifier.is_null()) { + app_identifier = PushMessagingAppIdentifier::Generate( + requesting_origin, service_worker_registration_id); + } + + if (push_subscription_count_ + pending_push_subscription_count_ >= + kMaxRegistrations) { + SubscribeEndWithError(std::move(register_callback), + blink::mojom::PushRegistrationStatus::LIMIT_REACHED); + return; + } + + if (!IsPermissionSet(requesting_origin, options->user_visible_only)) { + SubscribeEndWithError( + std::move(register_callback), + blink::mojom::PushRegistrationStatus::PERMISSION_DENIED); + return; + } + + DoSubscribe(std::move(app_identifier), std::move(options), + std::move(register_callback), + /* render_process_id= */ -1, /* render_frame_id= */ -1, + CONTENT_SETTING_ALLOW); +} + +blink::mojom::PermissionStatus PushMessagingServiceImpl::GetPermissionStatus( + const GURL& origin, + bool user_visible) { + if (!user_visible) + return blink::mojom::PermissionStatus::DENIED; + + // Because the Push API is tied to Service Workers, many usages of the API + // won't have an embedding origin at all. Only consider the requesting + // |origin| when checking whether permission to use the API has been granted. + return ToPermissionStatus( + PermissionManagerFactory::GetForProfile(profile_) + ->GetPermissionStatus(ContentSettingsType::NOTIFICATIONS, origin, + origin) + .content_setting); +} + +bool PushMessagingServiceImpl::SupportNonVisibleMessages() { + return false; +} + +void PushMessagingServiceImpl::DoSubscribe( + PushMessagingAppIdentifier app_identifier, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback register_callback, + int render_process_id, + int render_frame_id, + ContentSetting content_setting) { + if (content_setting != CONTENT_SETTING_ALLOW) { + SubscribeEndWithError( + std::move(register_callback), + blink::mojom::PushRegistrationStatus::PERMISSION_DENIED); + return; + } + + std::string application_server_key_string( + options->application_server_key.begin(), + options->application_server_key.end()); + + // TODO(peter): Move this check to the renderer process & Mojo message + // validation once the flag is always enabled, and remove the + // |render_process_id| and |render_frame_id| parameters from this method. + if (!push_messaging::IsVapidKey(application_server_key_string)) { + content::RenderFrameHost* render_frame_host = + content::RenderFrameHost::FromID(render_process_id, render_frame_id); + if (base::FeatureList::IsEnabled( + features::kPushMessagingDisallowSenderIDs)) { + if (render_frame_host) { + render_frame_host->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kError, + kSenderIdRegistrationDisallowedMessage); + } + SubscribeEndWithError( + std::move(register_callback), + blink::mojom::PushRegistrationStatus::UNSUPPORTED_GCM_SENDER_ID); + return; + } else if (render_frame_host) { + render_frame_host->AddMessageToConsole( + blink::mojom::ConsoleMessageLevel::kWarning, + kSenderIdRegistrationDeprecatedMessage); + } + } + + IncreasePushSubscriptionCount(1, true /* is_pending */); + + // Set time to live for GCM registration + base::TimeDelta ttl = base::TimeDelta(); + + if (base::FeatureList::IsEnabled( + features::kPushSubscriptionWithExpirationTime)) { + app_identifier.set_expiration_time( + base::Time::Now() + kPushSubscriptionExpirationPeriodTimeDelta); + DCHECK(app_identifier.expiration_time()); + ttl = kPushSubscriptionExpirationPeriodTimeDelta; + } + + GetInstanceIDDriver() + ->GetInstanceID(app_identifier.app_id()) + ->GetToken( + push_messaging::NormalizeSenderInfo(application_server_key_string), + kGCMScope, ttl, {} /* flags */, + base::BindOnce(&PushMessagingServiceImpl::DidSubscribe, + weak_factory_.GetWeakPtr(), app_identifier, + application_server_key_string, + std::move(register_callback))); +} + +void PushMessagingServiceImpl::SubscribeEnd( + RegisterCallback callback, + const std::string& subscription_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth, + blink::mojom::PushRegistrationStatus status) { + std::move(callback).Run(subscription_id, endpoint, expiration_time, p256dh, + auth, status); +} + +void PushMessagingServiceImpl::SubscribeEndWithError( + RegisterCallback callback, + blink::mojom::PushRegistrationStatus status) { + SubscribeEnd(std::move(callback), std::string() /* subscription_id */, + GURL::EmptyGURL() /* endpoint */, + absl::nullopt /* expiration_time */, + std::vector<uint8_t>() /* p256dh */, + std::vector<uint8_t>() /* auth */, status); +} + +void PushMessagingServiceImpl::DidSubscribe( + const PushMessagingAppIdentifier& app_identifier, + const std::string& sender_id, + RegisterCallback callback, + const std::string& subscription_id, + InstanceID::Result result) { + DecreasePushSubscriptionCount(1, true /* was_pending */); + + blink::mojom::PushRegistrationStatus status = + blink::mojom::PushRegistrationStatus::SERVICE_ERROR; + + switch (result) { + case InstanceID::SUCCESS: { + const GURL endpoint = push_messaging::CreateEndpoint(subscription_id); + + // Make sure that this subscription has associated encryption keys prior + // to returning it to the developer - they'll need this information in + // order to send payloads to the user. + GetEncryptionInfoForAppId( + app_identifier.app_id(), sender_id, + base::BindOnce( + &PushMessagingServiceImpl::DidSubscribeWithEncryptionInfo, + weak_factory_.GetWeakPtr(), app_identifier, std::move(callback), + subscription_id, endpoint)); + return; + } + case InstanceID::INVALID_PARAMETER: + case InstanceID::DISABLED: + case InstanceID::ASYNC_OPERATION_PENDING: + case InstanceID::SERVER_ERROR: + case InstanceID::UNKNOWN_ERROR: + DLOG(ERROR) << "Push messaging subscription failed; InstanceID::Result = " + << result; + status = blink::mojom::PushRegistrationStatus::SERVICE_ERROR; + break; + case InstanceID::NETWORK_ERROR: + status = blink::mojom::PushRegistrationStatus::NETWORK_ERROR; + break; + } + + SubscribeEndWithError(std::move(callback), status); +} + +void PushMessagingServiceImpl::DidSubscribeWithEncryptionInfo( + const PushMessagingAppIdentifier& app_identifier, + RegisterCallback callback, + const std::string& subscription_id, + const GURL& endpoint, + std::string p256dh, + std::string auth_secret) { + if (p256dh.empty()) { + SubscribeEndWithError( + std::move(callback), + blink::mojom::PushRegistrationStatus::PUBLIC_KEY_UNAVAILABLE); + return; + } + + app_identifier.PersistToPrefs(profile_); + + IncreasePushSubscriptionCount(1, false /* is_pending */); + + SubscribeEnd(std::move(callback), subscription_id, endpoint, + app_identifier.expiration_time(), + std::vector<uint8_t>(p256dh.begin(), p256dh.end()), + std::vector<uint8_t>(auth_secret.begin(), auth_secret.end()), + blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE); +} + +// GetSubscriptionInfo methods ------------------------------------------------- + +void PushMessagingServiceImpl::GetSubscriptionInfo( + const GURL& origin, + int64_t service_worker_registration_id, + const std::string& sender_id, + const std::string& subscription_id, + SubscriptionInfoCallback callback) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, origin, service_worker_registration_id); + + if (app_identifier.is_null()) { + std::move(callback).Run( + false /* is_valid */, GURL::EmptyGURL() /*endpoint*/, + absl::nullopt /* expiration_time */, + std::vector<uint8_t>() /* p256dh */, std::vector<uint8_t>() /* auth */); + return; + } + + const GURL endpoint = push_messaging::CreateEndpoint(subscription_id); + const std::string& app_id = app_identifier.app_id(); + absl::optional<base::Time> expiration_time = app_identifier.expiration_time(); + + base::OnceCallback<void(bool)> validate_cb = + base::BindOnce(&PushMessagingServiceImpl::DidValidateSubscription, + weak_factory_.GetWeakPtr(), app_id, sender_id, endpoint, + expiration_time, std::move(callback)); + + if (PushMessagingAppIdentifier::UseInstanceID(app_id)) { + GetInstanceIDDriver()->GetInstanceID(app_id)->ValidateToken( + push_messaging::NormalizeSenderInfo(sender_id), kGCMScope, + subscription_id, std::move(validate_cb)); + } else { + GetGCMDriver()->ValidateRegistration( + app_id, {push_messaging::NormalizeSenderInfo(sender_id)}, + subscription_id, std::move(validate_cb)); + } +} + +void PushMessagingServiceImpl::DidValidateSubscription( + const std::string& app_id, + const std::string& sender_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + SubscriptionInfoCallback callback, + bool is_valid) { + if (!is_valid) { + std::move(callback).Run( + false /* is_valid */, GURL::EmptyGURL() /* endpoint */, + absl::nullopt /* expiration_time */, + std::vector<uint8_t>() /* p256dh */, std::vector<uint8_t>() /* auth */); + return; + } + + GetEncryptionInfoForAppId( + app_id, sender_id, + base::BindOnce(&PushMessagingServiceImpl::DidGetEncryptionInfo, + weak_factory_.GetWeakPtr(), endpoint, expiration_time, + std::move(callback))); +} + +void PushMessagingServiceImpl::DidGetEncryptionInfo( + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + SubscriptionInfoCallback callback, + std::string p256dh, + std::string auth_secret) const { + // I/O errors might prevent the GCM Driver from retrieving a key-pair. + bool is_valid = !p256dh.empty(); + std::move(callback).Run( + is_valid, endpoint, expiration_time, + std::vector<uint8_t>(p256dh.begin(), p256dh.end()), + std::vector<uint8_t>(auth_secret.begin(), auth_secret.end())); +} + +// Unsubscribe methods --------------------------------------------------------- + +void PushMessagingServiceImpl::Unsubscribe( + blink::mojom::PushUnregistrationReason reason, + const GURL& requesting_origin, + int64_t service_worker_registration_id, + const std::string& sender_id, + UnregisterCallback callback) { + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, requesting_origin, service_worker_registration_id); + + UnsubscribeInternal( + reason, requesting_origin, service_worker_registration_id, + app_identifier.is_null() ? std::string() : app_identifier.app_id(), + sender_id, std::move(callback)); +} + +void PushMessagingServiceImpl::UnsubscribeInternal( + blink::mojom::PushUnregistrationReason reason, + const GURL& origin, + int64_t service_worker_registration_id, + const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback) { + DCHECK(!app_id.empty() || (!origin.is_empty() && + service_worker_registration_id != + -1 /* kInvalidServiceWorkerRegistrationId */)) + << "Need an app_id and/or origin+service_worker_registration_id"; + + RecordUnsubscribeReason(reason); + + if (origin.is_empty() || + service_worker_registration_id == + -1 /* kInvalidServiceWorkerRegistrationId */) { + // Can't clear Service Worker database. + DidClearPushSubscriptionId(reason, app_id, sender_id, std::move(callback)); + return; + } + ClearPushSubscriptionId( + profile_, origin, service_worker_registration_id, + base::BindOnce(&PushMessagingServiceImpl::DidClearPushSubscriptionId, + weak_factory_.GetWeakPtr(), reason, app_id, sender_id, + std::move(callback))); +} + +void PushMessagingServiceImpl::DidClearPushSubscriptionId( + blink::mojom::PushUnregistrationReason reason, + const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback) { + if (app_id.empty()) { + // Without an |app_id|, we can neither delete the subscription from the + // PushMessagingAppIdentifier map, nor unsubscribe with the GCM Driver. + std::move(callback).Run( + blink::mojom::PushUnregistrationStatus::SUCCESS_WAS_NOT_REGISTERED); + return; + } + + // Delete the mapping for this app_id, to guarantee that no messages get + // delivered in future (even if unregistration fails). + // TODO(johnme): Instead of deleting these app ids, store them elsewhere, and + // retry unregistration if it fails due to network errors (crbug.com/465399). + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + bool was_subscribed = !app_identifier.is_null(); + if (was_subscribed) + app_identifier.DeleteFromPrefs(profile_); + + // Run the unsubscribe callback *before* asking the InstanceIDDriver/GCMDriver + // to unsubscribe, since that's a slow process involving network retries, and + // by this point enough local state has been deleted that the subscription is + // inactive. Note that DeliverMessageCallback automatically unsubscribes if + // messages are later received for a subscription that was locally deleted, + // so as long as messages keep getting sent to it, the unsubscription should + // eventually reach GCM servers even if this particular attempt fails. + std::move(callback).Run( + was_subscribed + ? blink::mojom::PushUnregistrationStatus::SUCCESS_UNREGISTERED + : blink::mojom::PushUnregistrationStatus::SUCCESS_WAS_NOT_REGISTERED); + + if (PushMessagingAppIdentifier::UseInstanceID(app_id)) { + GetInstanceIDDriver()->GetInstanceID(app_id)->DeleteID( + base::BindOnce(&PushMessagingServiceImpl::DidDeleteID, + weak_factory_.GetWeakPtr(), app_id, was_subscribed)); + + } else { + auto unregister_callback = + base::BindOnce(&PushMessagingServiceImpl::DidUnregister, + weak_factory_.GetWeakPtr(), was_subscribed); +#if defined(OS_ANDROID) + // On Android the backend is different, and requires the original sender_id. + // DidGetSenderIdUnexpectedUnsubscribe and + // DidDeleteServiceWorkerRegistration sometimes call us with an empty one. + if (sender_id.empty()) { + std::move(unregister_callback).Run(gcm::GCMClient::INVALID_PARAMETER); + } else { + GetGCMDriver()->UnregisterWithSenderId( + app_id, push_messaging::NormalizeSenderInfo(sender_id), + std::move(unregister_callback)); + } +#else + GetGCMDriver()->Unregister(app_id, std::move(unregister_callback)); +#endif + } +} + +void PushMessagingServiceImpl::DidUnregister(bool was_subscribed, + gcm::GCMClient::Result result) { + RecordUnsubscribeGCMResult(result); + DidUnsubscribe(std::string() /* app_id_when_instance_id */, was_subscribed); +} + +void PushMessagingServiceImpl::DidDeleteID(const std::string& app_id, + bool was_subscribed, + InstanceID::Result result) { + RecordUnsubscribeIIDResult(result); + // DidUnsubscribe must be run asynchronously when passing a non-empty + // |app_id_when_instance_id|, since it calls + // InstanceIDDriver::RemoveInstanceID which deletes the InstanceID itself. + // Calling that immediately would cause a use-after-free in our caller. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&PushMessagingServiceImpl::DidUnsubscribe, + weak_factory_.GetWeakPtr(), app_id, was_subscribed)); +} + +void PushMessagingServiceImpl::DidUnsubscribe( + const std::string& app_id_when_instance_id, + bool was_subscribed) { + if (!app_id_when_instance_id.empty()) + GetInstanceIDDriver()->RemoveInstanceID(app_id_when_instance_id); + + if (was_subscribed) + DecreasePushSubscriptionCount(1, false /* was_pending */); + + if (!unsubscribe_callback_for_testing_.is_null()) + std::move(unsubscribe_callback_for_testing_).Run(); +} + +void PushMessagingServiceImpl::SetUnsubscribeCallbackForTesting( + base::OnceClosure callback) { + unsubscribe_callback_for_testing_ = std::move(callback); +} + +// DidDeleteServiceWorkerRegistration methods ---------------------------------- + +void PushMessagingServiceImpl::DidDeleteServiceWorkerRegistration( + const GURL& origin, + int64_t service_worker_registration_id) { + const PushMessagingAppIdentifier& app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker( + profile_, origin, service_worker_registration_id); + if (app_identifier.is_null()) { + if (!service_worker_unregistered_callback_for_testing_.is_null()) + service_worker_unregistered_callback_for_testing_.Run(); + return; + } + // Note this will not fully unsubscribe pre-InstanceID subscriptions on + // Android from GCM, as that requires a sender_id. (Ideally we'd fetch it + // from the SWDB in some "before_unregistered" SWObserver event.) + UnsubscribeInternal( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_UNREGISTERED, + origin, service_worker_registration_id, app_identifier.app_id(), + std::string() /* sender_id */, + base::BindOnce(&UnregisterCallbackToClosure, + service_worker_unregistered_callback_for_testing_.is_null() + ? base::DoNothing() + : service_worker_unregistered_callback_for_testing_)); +} + +void PushMessagingServiceImpl::SetServiceWorkerUnregisteredCallbackForTesting( + base::RepeatingClosure callback) { + service_worker_unregistered_callback_for_testing_ = std::move(callback); +} + +// DidDeleteServiceWorkerDatabase methods -------------------------------------- + +void PushMessagingServiceImpl::DidDeleteServiceWorkerDatabase() { + std::vector<PushMessagingAppIdentifier> app_identifiers = + PushMessagingAppIdentifier::GetAll(profile_); + + base::RepeatingClosure completed_closure = base::BarrierClosure( + app_identifiers.size(), + service_worker_database_wiped_callback_for_testing_.is_null() + ? base::DoNothing() + : service_worker_database_wiped_callback_for_testing_); + + for (const PushMessagingAppIdentifier& app_identifier : app_identifiers) { + // Note this will not fully unsubscribe pre-InstanceID subscriptions on + // Android from GCM, as that requires a sender_id. We can't fetch those from + // the Service Worker database anymore as it's been deleted. + UnsubscribeInternal( + blink::mojom::PushUnregistrationReason::SERVICE_WORKER_DATABASE_WIPED, + app_identifier.origin(), + app_identifier.service_worker_registration_id(), + app_identifier.app_id(), std::string() /* sender_id */, + base::BindOnce(&UnregisterCallbackToClosure, completed_closure)); + } +} + +void PushMessagingServiceImpl::SetServiceWorkerDatabaseWipedCallbackForTesting( + base::RepeatingClosure callback) { + service_worker_database_wiped_callback_for_testing_ = std::move(callback); +} + +// OnContentSettingChanged methods --------------------------------------------- + +void PushMessagingServiceImpl::OnContentSettingChanged( + const ContentSettingsPattern& primary_pattern, + const ContentSettingsPattern& secondary_pattern, + ContentSettingsTypeSet content_type_set) { + DCHECK(primary_pattern.IsValid()); + if (!content_type_set.Contains(ContentSettingsType::NOTIFICATIONS)) + return; + + std::vector<PushMessagingAppIdentifier> all_app_identifiers = + PushMessagingAppIdentifier::GetAll(profile_); + + base::RepeatingClosure barrier_closure = base::BarrierClosure( + all_app_identifiers.size(), + content_setting_changed_callback_for_testing_.is_null() + ? base::DoNothing() + : content_setting_changed_callback_for_testing_); + + for (const PushMessagingAppIdentifier& app_identifier : all_app_identifiers) { + if (!primary_pattern.Matches(app_identifier.origin())) { + barrier_closure.Run(); + continue; + } + + if (IsPermissionSet(app_identifier.origin())) { + barrier_closure.Run(); + continue; + } + + UnexpectedChange(app_identifier, + blink::mojom::PushUnregistrationReason::PERMISSION_REVOKED, + barrier_closure); + } +} + +void PushMessagingServiceImpl::UnexpectedUnsubscribe( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushUnregistrationReason reason, + UnregisterCallback unregister_callback) { + // When `pushsubscriptionchange` is supported by default, get |sender_id| from + // GetPushSubscriptionFromAppIdentifier callback and do not get the info from + // IO twice + bool need_sender_id = false; +#if defined(OS_ANDROID) + need_sender_id = + !PushMessagingAppIdentifier::UseInstanceID(app_identifier.app_id()); +#endif + if (need_sender_id) { + GetSenderId( + profile_, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + base::BindOnce( + &PushMessagingServiceImpl::DidGetSenderIdUnexpectedUnsubscribe, + weak_factory_.GetWeakPtr(), app_identifier, reason, + std::move(unregister_callback))); + } else { + UnsubscribeInternal(reason, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + app_identifier.app_id(), + std::string() /* sender_id */, + std::move(unregister_callback)); + } +} + +void PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifier( + const PushMessagingAppIdentifier& app_identifier, + base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> + subscription_cb) { + GetSWData(profile_, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + base::BindOnce(&PushMessagingServiceImpl::DidGetSWData, + weak_factory_.GetWeakPtr(), app_identifier, + std::move(subscription_cb))); +} + +void PushMessagingServiceImpl::DidGetSWData( + const PushMessagingAppIdentifier& app_identifier, + base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> subscription_cb, + const std::string& sender_id, + const std::string& subscription_id) { + // SW Database was corrupted, return immediately + if (sender_id.empty() || subscription_id.empty()) { + std::move(subscription_cb).Run(nullptr /* subscription */); + return; + } + GetSubscriptionInfo( + app_identifier.origin(), app_identifier.service_worker_registration_id(), + sender_id, subscription_id, + base::BindOnce( + &PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifierEnd, + weak_factory_.GetWeakPtr(), std::move(subscription_cb), sender_id)); +} + +void PushMessagingServiceImpl::GetPushSubscriptionFromAppIdentifierEnd( + base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback, + const std::string& sender_id, + bool is_valid, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth) { + if (!is_valid) { + // TODO(viviy): Log error in UMA + std::move(callback).Run(nullptr /* subscription */); + return; + } + + std::move(callback).Run(blink::mojom::PushSubscription::New( + endpoint, expiration_time, push_messaging::MakeOptions(sender_id), p256dh, + auth)); +} + +void PushMessagingServiceImpl::FirePushSubscriptionChange( + const PushMessagingAppIdentifier& app_identifier, + base::OnceClosure completed_closure, + blink::mojom::PushSubscriptionPtr new_subscription, + blink::mojom::PushSubscriptionPtr old_subscription) { + // Ensure |completed_closure| is run after this function + base::ScopedClosureRunner scoped_closure(std::move(completed_closure)); + + if (!base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent)) + return; + + if (app_identifier.is_null()) { + FirePushSubscriptionChangeCallback( + app_identifier, blink::mojom::PushEventStatus::UNKNOWN_APP_ID); + return; + } + + profile_->FirePushSubscriptionChangeEvent( + app_identifier.origin(), app_identifier.service_worker_registration_id(), + std::move(new_subscription), std::move(old_subscription), + base::BindOnce( + &PushMessagingServiceImpl::FirePushSubscriptionChangeCallback, + weak_factory_.GetWeakPtr(), app_identifier)); +} + +void PushMessagingServiceImpl::FirePushSubscriptionChangeCallback( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushEventStatus status) { + // Log Data in UMA + RecordPushSubcriptionChangeStatus(status); +} + +void PushMessagingServiceImpl::DidGetSenderIdUnexpectedUnsubscribe( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushUnregistrationReason reason, + UnregisterCallback callback, + const std::string& sender_id) { + // Unsubscribe the PushMessagingAppIdentifier with the push service. + // It's possible for GetSenderId to have failed and sender_id to be empty, if + // cookies (and the SW database) for an origin got cleared before permissions + // are cleared for the origin. In that case for legacy GCM registrations on + // Android, Unsubscribe will just delete the app identifier to block future + // messages. + // TODO(johnme): Auto-unregister before SW DB is cleared (crbug.com/402458). + UnsubscribeInternal(reason, app_identifier.origin(), + app_identifier.service_worker_registration_id(), + app_identifier.app_id(), sender_id, std::move(callback)); +} + +void PushMessagingServiceImpl::SetContentSettingChangedCallbackForTesting( + base::RepeatingClosure callback) { + content_setting_changed_callback_for_testing_ = std::move(callback); +} + +// KeyedService methods ------------------------------------------------------- + +void PushMessagingServiceImpl::Shutdown() { + GetGCMDriver()->RemoveAppHandler(kPushMessagingAppIdentifierPrefix); + HostContentSettingsMapFactory::GetForProfile(profile_)->RemoveObserver(this); +} + +// content::NotificationObserver methods --------------------------------------- + +void PushMessagingServiceImpl::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK_EQ(chrome::NOTIFICATION_APP_TERMINATING, type); + shutdown_started_ = true; +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + in_flight_keep_alive_.reset(); + in_flight_profile_keep_alive_.reset(); +#endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) +} + +// OnSubscriptionInvalidation methods ------------------------------------------ + +void PushMessagingServiceImpl::OnSubscriptionInvalidation( + const std::string& app_id) { + DCHECK(base::FeatureList::IsEnabled(features::kPushSubscriptionChangeEvent)) + << "It is not allowed to call this method when " + "features::kPushSubscriptionChangeEvent is disabled."; + PushMessagingAppIdentifier old_app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, app_id); + if (old_app_identifier.is_null()) + return; + + GetSenderId(profile_, old_app_identifier.origin(), + old_app_identifier.service_worker_registration_id(), + base::BindOnce(&PushMessagingServiceImpl::GetOldSubscription, + weak_factory_.GetWeakPtr(), old_app_identifier)); +} + +void PushMessagingServiceImpl::GetOldSubscription( + PushMessagingAppIdentifier old_app_identifier, + const std::string& sender_id) { + GetPushSubscriptionFromAppIdentifier( + old_app_identifier, + base::BindOnce(&PushMessagingServiceImpl::StartRefresh, + weak_factory_.GetWeakPtr(), old_app_identifier, + sender_id)); +} + +void PushMessagingServiceImpl::StartRefresh( + PushMessagingAppIdentifier old_app_identifier, + const std::string& sender_id, + blink::mojom::PushSubscriptionPtr old_subscription) { + // Generate a new app_identifier with the same information, but a different + // app_id. Expiration time will be overwritten by DoSubscribe, if the flag + // features::kPushSubscriptionWithExpiration time is enabled + PushMessagingAppIdentifier new_app_identifier = + PushMessagingAppIdentifier::Generate( + old_app_identifier.origin(), + old_app_identifier.service_worker_registration_id(), + absl::nullopt /* expiration_time */); + + refresher_.Refresh(old_app_identifier, new_app_identifier.app_id(), + sender_id); + + UpdateSubscription( + new_app_identifier, push_messaging::MakeOptions(sender_id), + base::BindOnce(&PushMessagingServiceImpl::DidUpdateSubscription, + weak_factory_.GetWeakPtr(), new_app_identifier.app_id(), + old_app_identifier.app_id(), std::move(old_subscription), + sender_id)); +} + +void PushMessagingServiceImpl::UpdateSubscription( + PushMessagingAppIdentifier app_identifier, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback callback) { + // After getting a new GCM registration, update the |subscription_id| in SW + // database before running the callback + auto register_callback = base::BindOnce( + [](RegisterCallback cb, Profile* profile, PushMessagingAppIdentifier ai, + const std::string& registration_id, const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, const std::vector<uint8_t>& auth, + blink::mojom::PushRegistrationStatus status) { + base::OnceClosure closure = + base::BindOnce(std::move(cb), registration_id, endpoint, + expiration_time, p256dh, auth, status); + base::ScopedClosureRunner closure_runner(std::move(closure)); + if (status == + blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE) { + UpdatePushSubscriptionId(profile, ai.origin(), + ai.service_worker_registration_id(), + registration_id, closure_runner.Release()); + } + }, + std::move(callback), profile_, app_identifier); + // Subscribe using the new subscription information, this will overwrite + // the expiration time of |app_identifier| + DoSubscribe(app_identifier, std::move(options), std::move(register_callback), + -1 /* render_process_id */, -1 /* render_frame_id */, + CONTENT_SETTING_ALLOW); +} + +void PushMessagingServiceImpl::DidUpdateSubscription( + const std::string& new_app_id, + const std::string& old_app_id, + blink::mojom::PushSubscriptionPtr old_subscription, + const std::string& sender_id, + const std::string& registration_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth, + blink::mojom::PushRegistrationStatus status) { + // TODO(crbug.com/1122545): Currently, if |status| is unsuccessful, the old + // subscription remains in SW database and preferences and the refresh is + // aborted. Instead, one should abort the refresh and retry to refresh + // periodically. + if (status != + blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE) { + return; + } + + // Old subscription is now replaced locally by the new subscription + refresher_.OnSubscriptionUpdated(new_app_id); + + PushMessagingAppIdentifier new_app_identifier = + PushMessagingAppIdentifier::FindByAppId(profile_, new_app_id); + + // Callback for testing + base::OnceClosure callback = + (invalidation_callback_for_testing_) + ? std::move(invalidation_callback_for_testing_) + : base::DoNothing(); + + FirePushSubscriptionChange( + new_app_identifier, std::move(callback), + blink::mojom::PushSubscription::New( + endpoint, expiration_time, push_messaging::MakeOptions(sender_id), + p256dh, auth), + std::move(old_subscription)); +} + +// PushMessagingRefresher::Observer methods ------------------------------------ + +void PushMessagingServiceImpl::OnOldSubscriptionExpired( + const std::string& app_id, + const std::string& sender_id) { + // Unsubscribe without clearing SW database, since values of the new + // subscription are already saved there. + // After unsubscribing, the refresher will get notified. + UnsubscribeInternal( + blink::mojom::PushUnregistrationReason::REFRESH_FINISHED, + GURL::EmptyGURL() /* origin */, -1 /* service_worker_registration_id */, + app_id, sender_id, + base::BindOnce(&UnregisterCallbackToClosure, + base::BindOnce(&PushMessagingRefresher::OnUnsubscribed, + refresher_.GetWeakPtr(), app_id))); +} + +void PushMessagingServiceImpl::OnRefreshFinished( + const PushMessagingAppIdentifier& app_identifier) { + // TODO(viviy): Log data in UMA +} + +void PushMessagingServiceImpl::SetInvalidationCallbackForTesting( + base::OnceClosure callback) { + invalidation_callback_for_testing_ = std::move(callback); +} + +// Helper methods -------------------------------------------------------------- + +void PushMessagingServiceImpl::SetRemoveExpiredSubscriptionsCallbackForTesting( + base::OnceClosure closure) { + remove_expired_subscriptions_callback_for_testing_ = std::move(closure); +} + +// Assumes user_visible always since this is just meant to check +// if the permission was previously granted and not revoked. +bool PushMessagingServiceImpl::IsPermissionSet(const GURL& origin, + bool user_visible) { + return GetPermissionStatus(origin, user_visible) == + blink::mojom::PermissionStatus::GRANTED; +} + +void PushMessagingServiceImpl::GetEncryptionInfoForAppId( + const std::string& app_id, + const std::string& sender_id, + gcm::GCMEncryptionProvider::EncryptionInfoCallback callback) { + if (PushMessagingAppIdentifier::UseInstanceID(app_id)) { + GetInstanceIDDriver()->GetInstanceID(app_id)->GetEncryptionInfo( + push_messaging::NormalizeSenderInfo(sender_id), std::move(callback)); + } else { + GetGCMDriver()->GetEncryptionInfo(app_id, std::move(callback)); + } +} + +gcm::GCMDriver* PushMessagingServiceImpl::GetGCMDriver() const { + gcm::GCMProfileService* gcm_profile_service = + gcm::GCMProfileServiceFactory::GetForProfile(profile_); + CHECK(gcm_profile_service); + CHECK(gcm_profile_service->driver()); + return gcm_profile_service->driver(); +} + +instance_id::InstanceIDDriver* PushMessagingServiceImpl::GetInstanceIDDriver() + const { + instance_id::InstanceIDProfileService* instance_id_profile_service = + instance_id::InstanceIDProfileServiceFactory::GetForProfile(profile_); + CHECK(instance_id_profile_service); + CHECK(instance_id_profile_service->driver()); + return instance_id_profile_service->driver(); +} + +content::DevToolsBackgroundServicesContext* +PushMessagingServiceImpl::GetDevToolsContext(const GURL& origin) const { + auto* storage_partition = profile_->GetStoragePartitionForUrl(origin); + if (!storage_partition) + return nullptr; + + auto* devtools_context = + storage_partition->GetDevToolsBackgroundServicesContext(); + + if (!devtools_context->IsRecording( + content::DevToolsBackgroundService::kPushMessaging)) { + return nullptr; + } + + return devtools_context; +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h new file mode 100644 index 00000000000..a99e6e578f4 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_impl.h @@ -0,0 +1,475 @@ +// Copyright 2014 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_ + +#include <stdint.h> +#include <memory> +#include <queue> +#include <utility> +#include <vector> + +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/containers/flat_map.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/scoped_observation.h" +#include "base/time/time.h" +#include "chrome/browser/permissions/abusive_origin_permission_revocation_request.h" +#include "chrome/browser/push_messaging/push_messaging_notification_manager.h" +#include "chrome/browser/push_messaging/push_messaging_refresher.h" +#include "chrome/common/buildflags.h" +#include "components/content_settings/core/browser/content_settings_observer.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/content_settings/core/common/content_settings_types.h" +#include "components/gcm_driver/common/gcm_message.h" +#include "components/gcm_driver/crypto/gcm_encryption_provider.h" +#include "components/gcm_driver/gcm_app_handler.h" +#include "components/gcm_driver/gcm_client.h" +#include "components/gcm_driver/instance_id/instance_id.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "content/public/browser/push_messaging_service.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom-forward.h" + +class GURL; +class Profile; +class PushMessagingAppIdentifier; +class PushMessagingServiceTest; +class ScopedKeepAlive; +class ScopedProfileKeepAlive; + +namespace blink { +namespace mojom { +enum class PushEventStatus; +enum class PushRegistrationStatus; +} // namespace mojom +} // namespace blink + +namespace content { +class DevToolsBackgroundServicesContext; +} // namespace content + +namespace gcm { +class GCMDriver; +} // namespace gcm + +namespace instance_id { +class InstanceIDDriver; +} // namespace instance_id + +namespace { +struct PendingMessage { + PendingMessage(std::string app_id, gcm::IncomingMessage message); + PendingMessage(const PendingMessage& other); + PendingMessage(PendingMessage&& other); + ~PendingMessage(); + + PendingMessage& operator=(PendingMessage&& other); + + std::string app_id; + gcm::IncomingMessage message; + base::Time received_time; +}; +} // namespace + +class PushMessagingServiceImpl : public content::PushMessagingService, + public gcm::GCMAppHandler, + public content_settings::Observer, + public KeyedService, + public content::NotificationObserver, + public PushMessagingRefresher::Observer { + public: + // If any Service Workers are using push, starts GCM and adds an app handler. + static void InitializeForProfile(Profile* profile); + + explicit PushMessagingServiceImpl(Profile* profile); + + PushMessagingServiceImpl(const PushMessagingServiceImpl&) = delete; + PushMessagingServiceImpl& operator=(const PushMessagingServiceImpl&) = delete; + + ~PushMessagingServiceImpl() override; + + // Check and remove subscriptions that are expired when |this| is initialized + void RemoveExpiredSubscriptions(); + + // Gets the permission status for the given |origin|. + blink::mojom::PermissionStatus GetPermissionStatus(const GURL& origin, + bool user_visible); + + // gcm::GCMAppHandler implementation. + void ShutdownHandler() override; + void OnStoreReset() override; + void OnMessage(const std::string& app_id, + const gcm::IncomingMessage& message) override; + void OnMessagesDeleted(const std::string& app_id) override; + void OnSendError( + const std::string& app_id, + const gcm::GCMClient::SendErrorDetails& send_error_details) override; + void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) override; + void OnMessageDecryptionFailed(const std::string& app_id, + const std::string& message_id, + const std::string& error_message) override; + bool CanHandle(const std::string& app_id) const override; + + // content::PushMessagingService implementation: + void SubscribeFromDocument(const GURL& requesting_origin, + int64_t service_worker_registration_id, + int render_process_id, + int render_frame_id, + blink::mojom::PushSubscriptionOptionsPtr options, + bool user_gesture, + RegisterCallback callback) override; + void SubscribeFromWorker(const GURL& requesting_origin, + int64_t service_worker_registration_id, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback callback) override; + void GetSubscriptionInfo(const GURL& origin, + int64_t service_worker_registration_id, + const std::string& sender_id, + const std::string& subscription_id, + SubscriptionInfoCallback callback) override; + void Unsubscribe(blink::mojom::PushUnregistrationReason reason, + const GURL& requesting_origin, + int64_t service_worker_registration_id, + const std::string& sender_id, + UnregisterCallback) override; + bool SupportNonVisibleMessages() override; + void DidDeleteServiceWorkerRegistration( + const GURL& origin, + int64_t service_worker_registration_id) override; + void DidDeleteServiceWorkerDatabase() override; + + // content_settings::Observer implementation. + void OnContentSettingChanged( + const ContentSettingsPattern& primary_pattern, + const ContentSettingsPattern& secondary_pattern, + ContentSettingsTypeSet content_type_set) override; + + // Fires the `pushsubscriptionchange` event to the associated service worker + // of |app_identifier|, which is the app identifier for |old_subscription| + // whereas |new_subscription| can be either null e.g. when a subscription is + // lost due to permission changes or a new subscription when it was refreshed. + void FirePushSubscriptionChange( + const PushMessagingAppIdentifier& app_identifier, + base::OnceClosure completed_closure, + blink::mojom::PushSubscriptionPtr new_subscription, + blink::mojom::PushSubscriptionPtr old_subscription); + + // KeyedService implementation. + void Shutdown() override; + + // content::NotificationObserver implementation + void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) override; + + // WARNING: Only call this function if features::kPushSubscriptionChangeEvent + // is enabled, will be later used by the Push Service to trigger subscription + // refreshes + void OnSubscriptionInvalidation(const std::string& app_id); + + // PushMessagingRefresher::Observer implementation + // Initiate unsubscribe task when old subscription becomes invalid + void OnOldSubscriptionExpired(const std::string& app_id, + const std::string& sender_id) override; + void OnRefreshFinished( + const PushMessagingAppIdentifier& app_identifier) override; + + void SetMessageCallbackForTesting(const base::RepeatingClosure& callback); + void SetUnsubscribeCallbackForTesting(base::OnceClosure callback); + void SetInvalidationCallbackForTesting(base::OnceClosure callback); + void SetContentSettingChangedCallbackForTesting( + base::RepeatingClosure callback); + void SetServiceWorkerUnregisteredCallbackForTesting( + base::RepeatingClosure callback); + void SetServiceWorkerDatabaseWipedCallbackForTesting( + base::RepeatingClosure callback); + void SetRemoveExpiredSubscriptionsCallbackForTesting( + base::OnceClosure closure); + + private: + friend class PushMessagingBrowserTestBase; + friend class PushMessagingServiceTest; + FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, NormalizeSenderInfo); + FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, PayloadEncryptionTest); + FRIEND_TEST_ALL_PREFIXES(PushMessagingServiceTest, + TestMultipleIncomingPushMessages); + + // A subscription is pending until it has succeeded or failed. + void IncreasePushSubscriptionCount(int add, bool is_pending); + void DecreasePushSubscriptionCount(int subtract, bool was_pending); + + // OnMessage methods --------------------------------------------------------- + + void DeliverMessageCallback(const std::string& app_id, + const GURL& requesting_origin, + int64_t service_worker_registration_id, + const gcm::IncomingMessage& message, + bool did_enqueue_message, + blink::mojom::PushEventStatus status); + + void DidHandleEnqueuedMessage( + const GURL& origin, + int64_t service_worker_registration_id, + base::OnceCallback<void(bool)> message_handled_callback, + bool did_show_generic_notification); + + void DidHandleMessage(const std::string& app_id, + const std::string& push_message_id, + bool did_show_generic_notification); + + void OnCheckedOriginForAbuse( + PendingMessage message, + AbusiveOriginPermissionRevocationRequest::Outcome outcome); + + void DeliverNextQueuedMessageForServiceWorkerRegistration( + const GURL& origin, + int64_t service_worker_registration_id); + + void CheckOriginForAbuseAndDispatchNextMessage(); + + // Subscribe methods --------------------------------------------------------- + + void DoSubscribe(PushMessagingAppIdentifier app_identifier, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback callback, + int render_process_id, + int render_frame_id, + ContentSetting permission_status); + + void SubscribeEnd(RegisterCallback callback, + const std::string& subscription_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth, + blink::mojom::PushRegistrationStatus status); + + void SubscribeEndWithError(RegisterCallback callback, + blink::mojom::PushRegistrationStatus status); + + void DidSubscribe(const PushMessagingAppIdentifier& app_identifier, + const std::string& sender_id, + RegisterCallback callback, + const std::string& subscription_id, + instance_id::InstanceID::Result result); + + void DidSubscribeWithEncryptionInfo( + const PushMessagingAppIdentifier& app_identifier, + RegisterCallback callback, + const std::string& subscription_id, + const GURL& endpoint, + std::string p256dh, + std::string auth_secret); + + // GetSubscriptionInfo methods ----------------------------------------------- + + void DidValidateSubscription( + const std::string& app_id, + const std::string& sender_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + SubscriptionInfoCallback callback, + bool is_valid); + + void DidGetEncryptionInfo(const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + SubscriptionInfoCallback callback, + std::string p256dh, + std::string auth_secret) const; + + // Unsubscribe methods ------------------------------------------------------- + + // |origin|, |service_worker_registration_id| and |app_id| should be provided + // whenever they can be obtained. It's valid for |origin| to be empty and + // |service_worker_registration_id| to be kInvalidServiceWorkerRegistrationId, + // or for app_id to be empty, but not both at once. + void UnsubscribeInternal(blink::mojom::PushUnregistrationReason reason, + const GURL& origin, + int64_t service_worker_registration_id, + const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback); + + void DidClearPushSubscriptionId(blink::mojom::PushUnregistrationReason reason, + const std::string& app_id, + const std::string& sender_id, + UnregisterCallback callback); + + void DidUnregister(bool was_subscribed, gcm::GCMClient::Result result); + void DidDeleteID(const std::string& app_id, + bool was_subscribed, + instance_id::InstanceID::Result result); + void DidUnsubscribe(const std::string& app_id_when_instance_id, + bool was_subscribed); + + // OnContentSettingChanged methods ------------------------------------------- + + void GetPushSubscriptionFromAppIdentifier( + const PushMessagingAppIdentifier& app_identifier, + base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback); + + void DidGetSWData( + const PushMessagingAppIdentifier& app_identifier, + base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback, + const std::string& sender_id, + const std::string& subscription_id); + + void GetPushSubscriptionFromAppIdentifierEnd( + base::OnceCallback<void(blink::mojom::PushSubscriptionPtr)> callback, + const std::string& sender_id, + bool is_valid, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth); + + // OnSubscriptionInvalidation methods----------------------------------------- + + void GetOldSubscription(PushMessagingAppIdentifier old_app_identifier, + const std::string& sender_id); + + // After gathering all relavent information to start the refresh, + // generate a new app id and initiate refresh + void StartRefresh(PushMessagingAppIdentifier old_app_identifier, + const std::string& sender_id, + blink::mojom::PushSubscriptionPtr old_subscription); + + // Makes a new susbcription and replaces the old subscription by new + // subscription in preferences and service worker database + void UpdateSubscription(PushMessagingAppIdentifier app_identifier, + blink::mojom::PushSubscriptionOptionsPtr options, + RegisterCallback callback); + + // After the subscription is updated, fire a `pushsubscriptionchange` event + // and notify the |refresher_| + void DidUpdateSubscription(const std::string& new_app_id, + const std::string& old_app_id, + blink::mojom::PushSubscriptionPtr old_subscription, + const std::string& sender_id, + const std::string& registration_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth, + blink::mojom::PushRegistrationStatus status); + // Helper methods ------------------------------------------------------------ + + // The subscription given in |identifier| will be unsubscribed (and a + // `pushsubscriptionchange` event fires if + // features::kPushSubscriptionChangeEvent is enabled) + void UnexpectedChange(PushMessagingAppIdentifier identifier, + blink::mojom::PushUnregistrationReason reason, + base::OnceClosure completed_closure); + + void UnexpectedUnsubscribe(const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushUnregistrationReason reason, + UnregisterCallback unregister_callback); + + void DidGetSenderIdUnexpectedUnsubscribe( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushUnregistrationReason reason, + UnregisterCallback callback, + const std::string& sender_id); + + void FirePushSubscriptionChangeCallback( + const PushMessagingAppIdentifier& app_identifier, + blink::mojom::PushEventStatus status); + + // Checks if a given origin is allowed to use Push. + bool IsPermissionSet(const GURL& origin, bool user_visible = true); + + // Wrapper around {GCMDriver, InstanceID}::GetEncryptionInfo. + void GetEncryptionInfoForAppId( + const std::string& app_id, + const std::string& sender_id, + gcm::GCMEncryptionProvider::EncryptionInfoCallback callback); + + gcm::GCMDriver* GetGCMDriver() const; + + instance_id::InstanceIDDriver* GetInstanceIDDriver() const; + + content::DevToolsBackgroundServicesContext* GetDevToolsContext( + const GURL& origin) const; + + // Testing methods ----------------------------------------------------------- + + using PushEventCallback = + base::OnceCallback<void(blink::mojom::PushEventStatus)>; + using MessageDispatchedCallback = + base::RepeatingCallback<void(const std::string& app_id, + const GURL& origin, + int64_t service_worker_registration_id, + absl::optional<std::string> payload, + PushEventCallback callback)>; + + // Callback to be invoked when a message has been dispatched. Enables tests to + // observe message delivery instead of delivering it to the Service Worker. + void SetMessageDispatchedCallbackForTesting( + const MessageDispatchedCallback& callback) { + message_dispatched_callback_for_testing_ = callback; + } + + raw_ptr<Profile> profile_; + std::unique_ptr<AbusiveOriginPermissionRevocationRequest> + abusive_origin_revocation_request_; + std::queue<PendingMessage> messages_pending_permission_check_; + + // {Origin, ServiceWokerRegistratonId} key for message delivery queue. This + // ensures that we only deliver one message at a time per ServiceWorker. + using MessageDeliveryQueueKey = std::pair<GURL, int64_t>; + + // Queue of pending messages per ServiceWorkerRegstration to be delivered one + // at a time. This allows us to enforce visibility requirements. + base::flat_map<MessageDeliveryQueueKey, std::queue<PendingMessage>> + message_delivery_queue_; + + int push_subscription_count_; + int pending_push_subscription_count_; + + base::RepeatingClosure message_callback_for_testing_; + base::OnceClosure unsubscribe_callback_for_testing_; + base::RepeatingClosure content_setting_changed_callback_for_testing_; + base::RepeatingClosure service_worker_unregistered_callback_for_testing_; + base::RepeatingClosure service_worker_database_wiped_callback_for_testing_; + base::OnceClosure remove_expired_subscriptions_callback_for_testing_; + base::OnceClosure invalidation_callback_for_testing_; + + PushMessagingNotificationManager notification_manager_; + + PushMessagingRefresher refresher_; + + base::ScopedObservation<PushMessagingRefresher, + PushMessagingRefresher::Observer> + refresh_observation_{this}; + + MessageDispatchedCallback message_dispatched_callback_for_testing_; + +#if BUILDFLAG(ENABLE_BACKGROUND_MODE) + // KeepAlive registered while we have in-flight push messages, to make sure + // we can finish processing them without being interrupted by BrowserProcess + // teardown. + std::unique_ptr<ScopedKeepAlive> in_flight_keep_alive_; + + // Same as ScopedKeepAlive, but prevents |profile_| from getting deleted. + std::unique_ptr<ScopedProfileKeepAlive> in_flight_profile_keep_alive_; +#endif + + content::NotificationRegistrar registrar_; + + // True when shutdown has started. Do not allow processing of incoming + // messages when this is true. + bool shutdown_started_ = false; + + base::WeakPtrFactory<PushMessagingServiceImpl> weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_SERVICE_IMPL_H_ diff --git a/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc b/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc new file mode 100644 index 00000000000..612589a5107 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_service_unittest.cc @@ -0,0 +1,480 @@ +// 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. + +#include "content/public/browser/push_messaging_service.h" + +#include <stdint.h> + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/cxx17_backports.h" +#include "base/run_loop.h" +#include "base/test/bind.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/gcm/gcm_profile_service_factory.h" +#include "chrome/browser/permissions/permission_manager_factory.h" +#include "chrome/browser/push_messaging/push_messaging_app_identifier.h" +#include "chrome/browser/push_messaging/push_messaging_features.h" +#include "chrome/browser/push_messaging/push_messaging_service_factory.h" +#include "chrome/browser/push_messaging/push_messaging_service_impl.h" +#include "chrome/browser/push_messaging/push_messaging_utils.h" +#include "chrome/test/base/testing_profile.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/gcm_driver/crypto/gcm_crypto_test_helpers.h" +#include "components/gcm_driver/fake_gcm_client_factory.h" +#include "components/gcm_driver/fake_gcm_profile_service.h" +#include "components/gcm_driver/gcm_profile_service.h" +#include "components/permissions/permission_manager.h" +#include "content/public/common/content_features.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/blink/public/mojom/push_messaging/push_messaging_status.mojom.h" + +#if defined(OS_ANDROID) +#include "components/gcm_driver/instance_id/instance_id_android.h" +#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h" +#endif // OS_ANDROID + +namespace { + +const char kTestOrigin[] = "https://example.com"; +const char kTestSenderId[] = "1234567890"; +const int64_t kTestServiceWorkerId = 42; +const char kTestPayload[] = "Hello, world!"; + +// NIST P-256 public key in uncompressed format per SEC1 2.3.3. +const uint8_t kTestP256Key[] = { + 0x04, 0x55, 0x52, 0x6A, 0xA5, 0x6E, 0x8E, 0xAA, 0x47, 0x97, 0x36, + 0x10, 0xC1, 0x66, 0x3C, 0x1E, 0x65, 0xBF, 0xA1, 0x7B, 0xEE, 0x48, + 0xC9, 0xC6, 0xBB, 0xBF, 0x02, 0x18, 0x53, 0x72, 0x1D, 0x0C, 0x7B, + 0xA9, 0xE3, 0x11, 0xB7, 0x03, 0x52, 0x21, 0xD3, 0x71, 0x90, 0x13, + 0xA8, 0xC1, 0xCF, 0xED, 0x20, 0xF7, 0x1F, 0xD1, 0x7F, 0xF2, 0x76, + 0xB6, 0x01, 0x20, 0xD8, 0x35, 0xA5, 0xD9, 0x3C, 0x43, 0xFD}; + +static_assert(sizeof(kTestP256Key) == 65, + "The fake public key must be a valid P-256 uncompressed point."); + +// URL-safe base64 encoded version of the |kTestP256Key|. +const char kTestEncodedP256Key[] = + "BFVSaqVujqpHlzYQwWY8HmW_oXvuSMnGu78CGFNyHQx7qeMRtwNSIdNxkBOowc_tIPcf0X_ydr" + "YBINg1pdk8Q_0"; + +// Implementation of the TestingProfile that provides the Push Messaging Service +// and the Permission Manager, both of which are required for the tests. +class PushMessagingTestingProfile : public TestingProfile { + public: + PushMessagingTestingProfile() = default; + + PushMessagingTestingProfile(const PushMessagingTestingProfile&) = delete; + PushMessagingTestingProfile& operator=(const PushMessagingTestingProfile&) = + delete; + + ~PushMessagingTestingProfile() override = default; + + PushMessagingServiceImpl* GetPushMessagingService() override { + return PushMessagingServiceFactory::GetForProfile(this); + } + + permissions::PermissionManager* GetPermissionControllerDelegate() override { + return PermissionManagerFactory::GetForProfile(this); + } +}; + +std::unique_ptr<KeyedService> BuildFakeGCMProfileService( + content::BrowserContext* context) { + return gcm::FakeGCMProfileService::Build(static_cast<Profile*>(context)); +} + +constexpr base::TimeDelta kPushEventHandleTime = base::Seconds(10); + +} // namespace + +class PushMessagingServiceTest : public ::testing::Test { + public: + PushMessagingServiceTest() { + // Always allow push notifications in the profile. + HostContentSettingsMap* host_content_settings_map = + HostContentSettingsMapFactory::GetForProfile(&profile_); + host_content_settings_map->SetDefaultContentSetting( + ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW); + + // Override the GCM Profile service so that we can send fake messages. + gcm::GCMProfileServiceFactory::GetInstance()->SetTestingFactory( + &profile_, base::BindRepeating(&BuildFakeGCMProfileService)); + } + + ~PushMessagingServiceTest() override = default; + + // Callback to use when the subscription may have been subscribed. + void DidRegister(std::string* subscription_id_out, + GURL* endpoint_out, + absl::optional<base::Time>* expiration_time_out, + std::vector<uint8_t>* p256dh_out, + std::vector<uint8_t>* auth_out, + base::OnceClosure done_callback, + const std::string& registration_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth, + blink::mojom::PushRegistrationStatus status) { + EXPECT_EQ(blink::mojom::PushRegistrationStatus::SUCCESS_FROM_PUSH_SERVICE, + status); + + *subscription_id_out = registration_id; + *expiration_time_out = expiration_time; + *endpoint_out = endpoint; + *p256dh_out = p256dh; + *auth_out = auth; + + std::move(done_callback).Run(); + } + + // Callback to use when observing messages dispatched by the push service. + void DidDispatchMessage( + std::string* app_id_out, + GURL* origin_out, + int64_t* service_worker_registration_id_out, + absl::optional<std::string>* payload_out, + const std::string& app_id, + const GURL& origin, + int64_t service_worker_registration_id, + absl::optional<std::string> payload, + PushMessagingServiceImpl::PushEventCallback callback) { + *app_id_out = app_id; + *origin_out = origin; + *service_worker_registration_id_out = service_worker_registration_id; + *payload_out = std::move(payload); + } + + class TestPushSubscription { + public: + std::string subscription_id_; + GURL endpoint_; + absl::optional<base::Time> expiration_time_; + std::vector<uint8_t> p256dh_; + std::vector<uint8_t> auth_; + TestPushSubscription(const std::string& subscription_id, + const GURL& endpoint, + const absl::optional<base::Time>& expiration_time, + const std::vector<uint8_t>& p256dh, + const std::vector<uint8_t>& auth) + : subscription_id_(subscription_id), + endpoint_(endpoint), + expiration_time_(expiration_time), + p256dh_(p256dh), + auth_(auth) {} + TestPushSubscription() = default; + }; + + void Subscribe(PushMessagingServiceImpl* push_service, + const GURL& origin, + TestPushSubscription* subscription = nullptr) { + std::string subscription_id; + GURL endpoint; + absl::optional<base::Time> expiration_time; + std::vector<uint8_t> p256dh, auth; + + base::RunLoop run_loop; + + auto options = blink::mojom::PushSubscriptionOptions::New(); + options->user_visible_only = true; + options->application_server_key = std::vector<uint8_t>( + kTestSenderId, + kTestSenderId + sizeof(kTestSenderId) / sizeof(char) - 1); + + push_service->SubscribeFromWorker( + origin, kTestServiceWorkerId, std::move(options), + base::BindOnce(&PushMessagingServiceTest::DidRegister, + base::Unretained(this), &subscription_id, &endpoint, + &expiration_time, &p256dh, &auth, + run_loop.QuitClosure())); + + EXPECT_EQ(0u, subscription_id.size()); // this must be asynchronous + + run_loop.Run(); + + ASSERT_GT(subscription_id.size(), 0u); + ASSERT_TRUE(endpoint.is_valid()); + ASSERT_GT(endpoint.spec().size(), 0u); + ASSERT_GT(p256dh.size(), 0u); + ASSERT_GT(auth.size(), 0u); + + if (subscription) { + subscription->subscription_id_ = subscription_id; + subscription->endpoint_ = endpoint; + subscription->p256dh_ = p256dh; + subscription->auth_ = auth; + } + } + + protected: + PushMessagingTestingProfile* profile() { return &profile_; } + + content::BrowserTaskEnvironment& task_environment() { + return task_environment_; + } + + private: + content::BrowserTaskEnvironment task_environment_{ + base::test::TaskEnvironment::TimeSource::MOCK_TIME}; + PushMessagingTestingProfile profile_; + +#if defined(OS_ANDROID) + instance_id::InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting + block_async_; +#endif // OS_ANDROID +}; + +// Fails too often on Linux TSAN builder: http://crbug.com/1211350. +#if defined(OS_LINUX) && defined(THREAD_SANITIZER) +#define MAYBE_PayloadEncryptionTest DISABLED_PayloadEncryptionTest +#else +#define MAYBE_PayloadEncryptionTest PayloadEncryptionTest +#endif +TEST_F(PushMessagingServiceTest, MAYBE_PayloadEncryptionTest) { + PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService(); + ASSERT_TRUE(push_service); + + const GURL origin(kTestOrigin); + + // (1) Make sure that |kExampleOrigin| has access to use Push Messaging. + ASSERT_EQ(blink::mojom::PermissionStatus::GRANTED, + push_service->GetPermissionStatus(origin, true /* user_visible */)); + + // (2) Subscribe for Push Messaging, and verify that we've got the required + // information in order to be able to create encrypted messages. + TestPushSubscription subscription; + Subscribe(push_service, origin, &subscription); + + // (3) Encrypt a message using the public key and authentication secret that + // are associated with the subscription. + + gcm::IncomingMessage message; + message.sender_id = kTestSenderId; + + ASSERT_TRUE(gcm::CreateEncryptedPayloadForTesting( + kTestPayload, + base::StringPiece(reinterpret_cast<char*>(subscription.p256dh_.data()), + subscription.p256dh_.size()), + base::StringPiece(reinterpret_cast<char*>(subscription.auth_.data()), + subscription.auth_.size()), + &message)); + + ASSERT_GT(message.raw_data.size(), 0u); + ASSERT_NE(kTestPayload, message.raw_data); + ASSERT_FALSE(message.decrypted); + + // (4) Find the app_id that has been associated with the subscription. + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin, + kTestServiceWorkerId); + + ASSERT_FALSE(app_identifier.is_null()); + + std::string app_id; + GURL dispatched_origin; + int64_t service_worker_registration_id; + absl::optional<std::string> payload; + + // (5) Observe message dispatchings from the Push Messaging service, and + // then dispatch the |message| on the GCM driver as if it had actually + // been received by Google Cloud Messaging. + push_service->SetMessageDispatchedCallbackForTesting(base::BindRepeating( + &PushMessagingServiceTest::DidDispatchMessage, base::Unretained(this), + &app_id, &dispatched_origin, &service_worker_registration_id, &payload)); + + gcm::FakeGCMProfileService* fake_profile_service = + static_cast<gcm::FakeGCMProfileService*>( + gcm::GCMProfileServiceFactory::GetForProfile(profile())); + + fake_profile_service->DispatchMessage(app_identifier.app_id(), message); + + base::RunLoop().RunUntilIdle(); + + // (6) Verify that the message, as received by the Push Messaging Service, has + // indeed been decrypted by the GCM Driver, and has been forwarded to the + // Service Worker that has been associated with the subscription. + EXPECT_EQ(app_identifier.app_id(), app_id); + EXPECT_EQ(origin, dispatched_origin); + EXPECT_EQ(service_worker_registration_id, kTestServiceWorkerId); + + EXPECT_TRUE(payload); + EXPECT_EQ(kTestPayload, *payload); +} + +TEST_F(PushMessagingServiceTest, NormalizeSenderInfo) { + PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService(); + ASSERT_TRUE(push_service); + + std::string p256dh(kTestP256Key, kTestP256Key + base::size(kTestP256Key)); + ASSERT_EQ(65u, p256dh.size()); + + // NIST P-256 public keys in uncompressed format will be encoded using the + // URL-safe base64 encoding by the normalization function. + EXPECT_EQ(kTestEncodedP256Key, push_messaging::NormalizeSenderInfo(p256dh)); + + // Any other value, binary or not, will be passed through as-is. + EXPECT_EQ("1234567890", push_messaging::NormalizeSenderInfo("1234567890")); + EXPECT_EQ("foo@bar.com", push_messaging::NormalizeSenderInfo("foo@bar.com")); + + p256dh[0] = 0x05; // invalidate |p256dh| as a public key. + + EXPECT_EQ(p256dh, push_messaging::NormalizeSenderInfo(p256dh)); +} + +// Fails too often on Linux TSAN builder: http://crbug.com/1211350. +#if defined(OS_LINUX) && defined(THREAD_SANITIZER) +#define MAYBE_RemoveExpiredSubscriptions DISABLED_RemoveExpiredSubscriptions +#else +#define MAYBE_RemoveExpiredSubscriptions RemoveExpiredSubscriptions +#endif +TEST_F(PushMessagingServiceTest, MAYBE_RemoveExpiredSubscriptions) { + // (1) Enable push subscriptions with expiration time and + // `pushsubscriptionchange` events + base::test::ScopedFeatureList scoped_feature_list_; + scoped_feature_list_.InitWithFeatures( + /* enabled features */ + {features::kPushSubscriptionWithExpirationTime, + features::kPushSubscriptionChangeEvent}, + /* disabled features */ + {}); + + // (2) Set up push service and test origin + PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService(); + ASSERT_TRUE(push_service); + const GURL origin(kTestOrigin); + + // (3) Subscribe origin to push service and find corresponding + // |app_identifier| + Subscribe(push_service, origin); + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin, + kTestServiceWorkerId); + ASSERT_FALSE(app_identifier.is_null()); + + // (4) Manually set the time as expired, save the time in preferences + app_identifier.set_expiration_time(base::Time::UnixEpoch()); + app_identifier.PersistToPrefs(profile()); + ASSERT_EQ(1u, PushMessagingAppIdentifier::GetCount(profile())); + + // (3) Remove all expired subscriptions + base::RunLoop run_loop; + push_service->SetRemoveExpiredSubscriptionsCallbackForTesting( + run_loop.QuitClosure()); + push_service->RemoveExpiredSubscriptions(); + run_loop.Run(); + + // (5) We expect the subscription to be deleted + ASSERT_EQ(0u, PushMessagingAppIdentifier::GetCount(profile())); + PushMessagingAppIdentifier deleted_identifier = + PushMessagingAppIdentifier::FindByAppId(profile(), + app_identifier.app_id()); + EXPECT_TRUE(deleted_identifier.is_null()); +} + +TEST_F(PushMessagingServiceTest, TestMultipleIncomingPushMessages) { + base::HistogramTester histograms; + PushMessagingServiceImpl* push_service = profile()->GetPushMessagingService(); + ASSERT_TRUE(push_service); + + // Subscribe |origin| to push service. + const GURL origin(kTestOrigin); + Subscribe(push_service, origin); + PushMessagingAppIdentifier app_identifier = + PushMessagingAppIdentifier::FindByServiceWorker(profile(), origin, + kTestServiceWorkerId); + ASSERT_FALSE(app_identifier.is_null()); + + // Setup decrypted test message. + gcm::IncomingMessage message; + message.sender_id = kTestSenderId; + message.raw_data = "testdata"; + message.decrypted = true; + + // Setup callbacks for dispatch and handled push events. + auto dispatched_run_loop = std::make_unique<base::RunLoop>(); + auto handled_run_loop = std::make_unique<base::RunLoop>(); + PushMessagingServiceImpl::PushEventCallback handle_push_event; + + push_service->SetMessageDispatchedCallbackForTesting( + base::BindLambdaForTesting( + [&](const std::string& app_id, const GURL& origin, + int64_t service_worker_registration_id, + absl::optional<std::string> payload, + PushMessagingServiceImpl::PushEventCallback callback) { + handle_push_event = std::move(callback); + dispatched_run_loop->Quit(); + })); + + push_service->SetMessageCallbackForTesting( + base::BindLambdaForTesting([&]() { handled_run_loop->Quit(); })); + + // Simulate two incoming push messages at the same time. + push_service->OnMessage(app_identifier.app_id(), message); + push_service->OnMessage(app_identifier.app_id(), message); + + // First wait until we dispatched the first push message. + dispatched_run_loop->Run(); + dispatched_run_loop = std::make_unique<base::RunLoop>(); + auto handled_first = std::move(handle_push_event); + handle_push_event = PushMessagingServiceImpl::PushEventCallback(); + + histograms.ExpectUniqueTimeSample("PushMessaging.CheckOriginForAbuseTime", + base::Seconds(0), + /*expected_bucket_count=*/1); + histograms.ExpectUniqueTimeSample("PushMessaging.DeliverQueuedMessageTime", + base::Seconds(0), + /*expected_bucket_count=*/1); + + // Run all tasks until idle so we can verify that we don't dispatch the second + // push message until the first one is handled. + base::RunLoop().RunUntilIdle(); + EXPECT_FALSE(handle_push_event); + + // Simulate handling the first push event takes some time. + task_environment().FastForwardBy(kPushEventHandleTime); + + // Now signal that the first push event has been handled and wait until we + // checked for visibility requirements. + std::move(handled_first).Run(blink::mojom::PushEventStatus::SUCCESS); + handled_run_loop->Run(); + handled_run_loop = std::make_unique<base::RunLoop>(); + + histograms.ExpectUniqueTimeSample("PushMessaging.MessageHandledTime", + kPushEventHandleTime, + /*expected_bucket_count=*/1); + + // Simulate handling the second push event takes some time. + task_environment().FastForwardBy(kPushEventHandleTime); + + // Now wait until we dispatched the second push message and handle it too. + dispatched_run_loop->Run(); + std::move(handle_push_event).Run(blink::mojom::PushEventStatus::SUCCESS); + handled_run_loop->Run(); + + // Checking origins for abuse happens immediately on receiving a push message + // one at a time. Both messages do that instantly in this test. + histograms.ExpectTimeBucketCount("PushMessaging.CheckOriginForAbuseTime", + base::Seconds(0), + /*count=*/2); + // Delivering messages should be done in series so the second message should + // have waited for the first one to be handled. + histograms.ExpectTimeBucketCount("PushMessaging.DeliverQueuedMessageTime", + kPushEventHandleTime, + /*count=*/1); + // The total time from receiving until handling of the second message. + histograms.ExpectTimeBucketCount("PushMessaging.MessageHandledTime", + kPushEventHandleTime * 2, + /*count=*/1); +} diff --git a/chromium/chrome/browser/push_messaging/push_messaging_utils.cc b/chromium/chrome/browser/push_messaging/push_messaging_utils.cc new file mode 100644 index 00000000000..b54bf14acf3 --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_utils.cc @@ -0,0 +1,44 @@ +// 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. + +#include "chrome/browser/push_messaging/push_messaging_utils.h" +#include "base/base64url.h" +#include "chrome/browser/push_messaging/push_messaging_constants.h" +#include "url/gurl.h" + +namespace push_messaging { + +GURL CreateEndpoint(const std::string& subscription_id) { + const GURL endpoint(kPushMessagingGcmEndpoint + subscription_id); + DCHECK(endpoint.is_valid()); + return endpoint; +} + +blink::mojom::PushSubscriptionOptionsPtr MakeOptions( + const std::string& sender_id) { + return blink::mojom::PushSubscriptionOptions::New( + /*user_visible_only=*/true, + std::vector<uint8_t>(sender_id.begin(), sender_id.end())); +} + +bool IsVapidKey(const std::string& application_server_key) { + // VAPID keys are NIST P-256 public keys in uncompressed format (64 bytes), + // verified through its length and the 0x04 prefix. + return application_server_key.size() == 65 && + application_server_key[0] == 0x04; +} + +std::string NormalizeSenderInfo(const std::string& application_server_key) { + if (!IsVapidKey(application_server_key)) + return application_server_key; + + std::string encoded_application_server_key; + base::Base64UrlEncode(application_server_key, + base::Base64UrlEncodePolicy::OMIT_PADDING, + &encoded_application_server_key); + + return encoded_application_server_key; +} + +} // namespace push_messaging diff --git a/chromium/chrome/browser/push_messaging/push_messaging_utils.h b/chromium/chrome/browser/push_messaging/push_messaging_utils.h new file mode 100644 index 00000000000..805deeab47e --- /dev/null +++ b/chromium/chrome/browser/push_messaging/push_messaging_utils.h @@ -0,0 +1,34 @@ +// 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. + +#ifndef CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_ +#define CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_ + +#include <string> +#include "third_party/blink/public/mojom/push_messaging/push_messaging.mojom.h" + +class GURL; + +namespace push_messaging { + +// Returns the URL used to send push messages to the subscription identified +// by |subscription_id|. +GURL CreateEndpoint(const std::string& subscription_id); + +// Checks size and prefix to determine whether it is a VAPID key +bool IsVapidKey(const std::string& application_server_key); + +// Normalizes the |sender_info|. In most cases the |sender_info| will be +// passed through to the GCM Driver as-is, but NIST P-256 application server +// keys have to be encoded using the URL-safe variant of the base64 encoding. +std::string NormalizeSenderInfo(const std::string& sender_info); + +// Currently |user_visible_only| is always true, once silent pushes are +// enabled, get this information from SW database. +blink::mojom::PushSubscriptionOptionsPtr MakeOptions( + const std::string& sender_id); + +} // namespace push_messaging + +#endif // CHROME_BROWSER_PUSH_MESSAGING_PUSH_MESSAGING_UTILS_H_ diff --git a/chromium/chrome/browser/signin/DEPS b/chromium/chrome/browser/signin/DEPS new file mode 100644 index 00000000000..e2cc84fd222 --- /dev/null +++ b/chromium/chrome/browser/signin/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+ash/components/account_manager", +] diff --git a/chromium/chrome/browser/signin/DIR_METADATA b/chromium/chrome/browser/signin/DIR_METADATA new file mode 100644 index 00000000000..95ad5b66d16 --- /dev/null +++ b/chromium/chrome/browser/signin/DIR_METADATA @@ -0,0 +1 @@ +mixins: "//components/signin/COMMON_METADATA" diff --git a/chromium/chrome/browser/signin/OWNERS b/chromium/chrome/browser/signin/OWNERS new file mode 100644 index 00000000000..017d5a003db --- /dev/null +++ b/chromium/chrome/browser/signin/OWNERS @@ -0,0 +1,5 @@ +file://components/signin/OWNERS + +per-file chrome_proximity_auth_*=xiyuan@chromium.org +per-file easy_unlock_*=xiyuan@chromium.org +per-file signin_profile_attributes_updater*=msalama@chromium.org diff --git a/chromium/chrome/browser/signin/about_signin_internals_factory.cc b/chromium/chrome/browser/signin/about_signin_internals_factory.cc new file mode 100644 index 00000000000..7e62dc93ebf --- /dev/null +++ b/chromium/chrome/browser/signin/about_signin_internals_factory.cc @@ -0,0 +1,58 @@ +// Copyright (c) 2012 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. + +#include "chrome/browser/signin/about_signin_internals_factory.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/account_consistency_mode_manager_factory.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_error_controller_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/signin/core/browser/about_signin_internals.h" + +AboutSigninInternalsFactory::AboutSigninInternalsFactory() + : BrowserContextKeyedServiceFactory( + "AboutSigninInternals", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(ChromeSigninClientFactory::GetInstance()); + DependsOn(SigninErrorControllerFactory::GetInstance()); + DependsOn(AccountReconcilorFactory::GetInstance()); + DependsOn(IdentityManagerFactory::GetInstance()); + DependsOn(AccountConsistencyModeManagerFactory::GetInstance()); +} + +AboutSigninInternalsFactory::~AboutSigninInternalsFactory() {} + +// static +AboutSigninInternals* AboutSigninInternalsFactory::GetForProfile( + Profile* profile) { + return static_cast<AboutSigninInternals*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +AboutSigninInternalsFactory* AboutSigninInternalsFactory::GetInstance() { + return base::Singleton<AboutSigninInternalsFactory>::get(); +} + +void AboutSigninInternalsFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* user_prefs) { + AboutSigninInternals::RegisterPrefs(user_prefs); +} + +KeyedService* AboutSigninInternalsFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + AboutSigninInternals* service = new AboutSigninInternals( + IdentityManagerFactory::GetForProfile(profile), + SigninErrorControllerFactory::GetForProfile(profile), + AccountConsistencyModeManager::GetMethodForProfile(profile), + ChromeSigninClientFactory::GetForProfile(profile), + AccountReconcilorFactory::GetForProfile(profile)); + return service; +} diff --git a/chromium/chrome/browser/signin/about_signin_internals_factory.h b/chromium/chrome/browser/signin/about_signin_internals_factory.h new file mode 100644 index 00000000000..c1b7cb5a71d --- /dev/null +++ b/chromium/chrome/browser/signin/about_signin_internals_factory.h @@ -0,0 +1,40 @@ +// Copyright (c) 2012 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. + +#ifndef CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class AboutSigninInternals; +class Profile; + +// Singleton that owns all AboutSigninInternals and associates them with +// Profiles. +class AboutSigninInternalsFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of AboutSigninInternals associated with this profile, + // creating one if none exists. + static AboutSigninInternals* GetForProfile(Profile* profile); + + // Returns an instance of the AboutSigninInternalsFactory singleton. + static AboutSigninInternalsFactory* GetInstance(); + + // Implementation of BrowserContextKeyedServiceFactory. + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + + private: + friend struct base::DefaultSingletonTraits<AboutSigninInternalsFactory>; + + AboutSigninInternalsFactory(); + ~AboutSigninInternalsFactory() override; + + // BrowserContextKeyedServiceFactory + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_ABOUT_SIGNIN_INTERNALS_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager.cc new file mode 100644 index 00000000000..6c1f0f04720 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager.cc @@ -0,0 +1,208 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/account_consistency_mode_manager.h" + +#include <string> + +#include "base/command_line.h" +#include "base/logging.h" +#include "base/metrics/field_trial_params.h" +#include "base/metrics/histogram_macros.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profiles_state.h" +#include "chrome/browser/signin/account_consistency_mode_manager_factory.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/common/pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "google_apis/google_api_keys.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/account_manager/account_manager_util.h" +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) && BUILDFLAG(ENABLE_MIRROR) +#error "Dice and Mirror cannot be both enabled." +#endif + +#if !BUILDFLAG(ENABLE_DICE_SUPPORT) && !BUILDFLAG(ENABLE_MIRROR) +#error "Either Dice or Mirror should be enabled." +#endif + +using signin::AccountConsistencyMethod; + +namespace { + +// By default, DICE is not enabled in builds lacking an API key. May be set to +// true for tests. +bool g_ignore_missing_oauth_client_for_testing = false; + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +const char kAllowBrowserSigninArgument[] = "allow-browser-signin"; + +bool IsBrowserSigninAllowedByCommandLine() { + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + if (command_line->HasSwitch(kAllowBrowserSigninArgument)) { + std::string allowBrowserSignin = + command_line->GetSwitchValueASCII(kAllowBrowserSigninArgument); + return base::ToLowerASCII(allowBrowserSignin) == "true"; + } + // If the commandline flag is not provided, the default is true. + return true; +} + +// Returns true if Desktop Identity Consistency can be enabled for this build +// (i.e. if OAuth client ID and client secret are configured). +bool CanEnableDiceForBuild() { + if (g_ignore_missing_oauth_client_for_testing || + google_apis::HasOAuthClientConfigured()) { + return true; + } + + // Only log this once. + static bool logged_warning = []() { + LOG(WARNING) << "Desktop Identity Consistency cannot be enabled as no " + "OAuth client ID and client secret have been configured."; + return true; + }(); + ALLOW_UNUSED_LOCAL(logged_warning); + + return false; +} +#endif + +} // namespace + +// static +AccountConsistencyModeManager* AccountConsistencyModeManager::GetForProfile( + Profile* profile) { + return AccountConsistencyModeManagerFactory::GetForProfile(profile); +} + +AccountConsistencyModeManager::AccountConsistencyModeManager(Profile* profile) + : profile_(profile), + account_consistency_(signin::AccountConsistencyMethod::kDisabled), + account_consistency_initialized_(false) { + DCHECK(profile_); + DCHECK(ShouldBuildServiceForProfile(profile)); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + PrefService* prefs = profile->GetPrefs(); + // Propagate settings changes from the previous launch to the signin-allowed + // pref. + bool signin_allowed = IsDiceSignInAllowed() && + prefs->GetBoolean(prefs::kSigninAllowedOnNextStartup); + prefs->SetBoolean(prefs::kSigninAllowed, signin_allowed); + + UMA_HISTOGRAM_BOOLEAN("Signin.SigninAllowed", signin_allowed); +#endif + + account_consistency_ = ComputeAccountConsistencyMethod(profile_); + DCHECK_EQ(account_consistency_, ComputeAccountConsistencyMethod(profile_)); + account_consistency_initialized_ = true; +} + +AccountConsistencyModeManager::~AccountConsistencyModeManager() {} + +// static +void AccountConsistencyModeManager::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterBooleanPref(prefs::kSigninAllowedOnNextStartup, true); +} + +// static +AccountConsistencyMethod AccountConsistencyModeManager::GetMethodForProfile( + Profile* profile) { + if (!ShouldBuildServiceForProfile(profile)) + return AccountConsistencyMethod::kDisabled; + + return AccountConsistencyModeManager::GetForProfile(profile) + ->GetAccountConsistencyMethod(); +} + +// static +bool AccountConsistencyModeManager::IsDiceEnabledForProfile(Profile* profile) { + return GetMethodForProfile(profile) == AccountConsistencyMethod::kDice; +} + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +// static +bool AccountConsistencyModeManager::IsDiceSignInAllowed() { + return CanEnableDiceForBuild() && IsBrowserSigninAllowedByCommandLine(); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +// static +bool AccountConsistencyModeManager::IsMirrorEnabledForProfile( + Profile* profile) { + return GetMethodForProfile(profile) == AccountConsistencyMethod::kMirror; +} + +// static +void AccountConsistencyModeManager::SetIgnoreMissingOAuthClientForTesting() { + g_ignore_missing_oauth_client_for_testing = true; +} + +// static +bool AccountConsistencyModeManager::ShouldBuildServiceForProfile( + Profile* profile) { + return profile->IsRegularProfile(); +} + +AccountConsistencyMethod +AccountConsistencyModeManager::GetAccountConsistencyMethod() { +#if BUILDFLAG(IS_CHROMEOS_ASH) + // TODO(https://crbug.com/860671): ChromeOS should use the cached value. + // Changing the value dynamically is not supported. + return ComputeAccountConsistencyMethod(profile_); +#else + // The account consistency method should not change during the lifetime of a + // profile. We always return the cached value, but still check that it did not + // change, in order to detect inconsisent states. See https://crbug.com/860471 + CHECK(account_consistency_initialized_); + CHECK_EQ(ComputeAccountConsistencyMethod(profile_), account_consistency_); + return account_consistency_; +#endif +} + +// static +signin::AccountConsistencyMethod +AccountConsistencyModeManager::ComputeAccountConsistencyMethod( + Profile* profile) { + DCHECK(ShouldBuildServiceForProfile(profile)); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (!ash::IsAccountManagerAvailable(profile)) + return AccountConsistencyMethod::kDisabled; +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) + // Account consistency is unavailable on Managed Guest Sessions and Public + // Sessions. + if (profiles::IsPublicSession()) + return AccountConsistencyMethod::kDisabled; +#endif + +#if BUILDFLAG(ENABLE_MIRROR) + return AccountConsistencyMethod::kMirror; +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + if (!profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)) { + VLOG(1) << "Desktop Identity Consistency disabled as sign-in to Chrome" + "is not allowed"; + return AccountConsistencyMethod::kDisabled; + } + + return AccountConsistencyMethod::kDice; +#endif + + NOTREACHED(); + return AccountConsistencyMethod::kDisabled; +} diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager.h b/chromium/chrome/browser/signin/account_consistency_mode_manager.h new file mode 100644 index 00000000000..8ca66aae9b9 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager.h @@ -0,0 +1,97 @@ +// Copyright 2017 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. + +#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_ + +#include "base/feature_list.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "build/buildflag.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/prefs/pref_member.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_buildflags.h" + +namespace user_prefs { +class PrefRegistrySyncable; +} + +class Profile; + +// Manages the account consistency mode for each profile. +class AccountConsistencyModeManager : public KeyedService { + public: + // Returns the AccountConsistencyModeManager associated with this profile. + // May return nullptr if there is none (e.g. in incognito). + static AccountConsistencyModeManager* GetForProfile(Profile* profile); + + explicit AccountConsistencyModeManager(Profile* profile); + + AccountConsistencyModeManager(const AccountConsistencyModeManager&) = delete; + AccountConsistencyModeManager& operator=( + const AccountConsistencyModeManager&) = delete; + + ~AccountConsistencyModeManager() override; + + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Helper method, shorthand for calling GetAccountConsistencyMethod(). + // TODO(crbug.com/1232361): Migrate usages to + // `IdentityManager::GetAccountConsistency`. + static signin::AccountConsistencyMethod GetMethodForProfile(Profile* profile); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + // This is a pre-requisite of IsDiceEnabledForProfile(), independent of + // particular profile type or profile prefs. + static bool IsDiceSignInAllowed(); +#endif + + // If true, then account management is done through Gaia webpages. + // Can only be used on the UI thread. + // Returns false if |profile| is in Guest or Incognito mode. + // A given |profile| will have only one of Mirror or Dice consistency + // behaviour enabled. + static bool IsDiceEnabledForProfile(Profile* profile); + + // Returns |true| if Mirror account consistency is enabled for |profile|. + // Can only be used on the UI thread. + // A given |profile| will have only one of Mirror or Dice consistency + // behaviour enabled. + static bool IsMirrorEnabledForProfile(Profile* profile); + + // By default, Desktop Identity Consistency (aka Dice) is not enabled in + // builds lacking an API key. For testing, set to have Dice enabled in tests. + static void SetIgnoreMissingOAuthClientForTesting(); + + // Returns true is the AccountConsistencyModeManager should be instantiated + // for the profile. Guest, incognito and system sessions do not instantiate + // the service. + static bool ShouldBuildServiceForProfile(Profile* profile); + + private: + FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest, + MigrateAtCreation); + FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest, + SigninAllowedChangesDiceState); + FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest, + AllowBrowserSigninSwitch); + FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest, + DiceEnabledForNewProfiles); + + // Returns the account consistency method for the current profile. + signin::AccountConsistencyMethod GetAccountConsistencyMethod(); + + // Computes the account consistency method for the current profile. This is + // only called from the constructor, the account consistency method cannot + // change during the lifetime of a profile. + static signin::AccountConsistencyMethod ComputeAccountConsistencyMethod( + Profile* profile); + + raw_ptr<Profile> profile_; + signin::AccountConsistencyMethod account_consistency_; + bool account_consistency_initialized_; +}; + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_H_ diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc new file mode 100644 index 00000000000..10178f34732 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.cc @@ -0,0 +1,53 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/account_consistency_mode_manager_factory.h" + +#include "base/check.h" +#include "build/build_config.h" +#include "chrome/browser/profiles/profile.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +// static +AccountConsistencyModeManagerFactory* +AccountConsistencyModeManagerFactory::GetInstance() { + return base::Singleton<AccountConsistencyModeManagerFactory>::get(); +} + +// static +AccountConsistencyModeManager* +AccountConsistencyModeManagerFactory::GetForProfile(Profile* profile) { + DCHECK(profile); + return static_cast<AccountConsistencyModeManager*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +AccountConsistencyModeManagerFactory::AccountConsistencyModeManagerFactory() + : BrowserContextKeyedServiceFactory( + "AccountConsistencyModeManager", + BrowserContextDependencyManager::GetInstance()) {} + +AccountConsistencyModeManagerFactory::~AccountConsistencyModeManagerFactory() = + default; + +KeyedService* AccountConsistencyModeManagerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + DCHECK(!context->IsOffTheRecord()); + Profile* profile = Profile::FromBrowserContext(context); + + return AccountConsistencyModeManager::ShouldBuildServiceForProfile(profile) + ? new AccountConsistencyModeManager(profile) + : nullptr; +} + +void AccountConsistencyModeManagerFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + AccountConsistencyModeManager::RegisterProfilePrefs(registry); +} + +bool AccountConsistencyModeManagerFactory::ServiceIsCreatedWithBrowserContext() + const { + return true; +} diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h new file mode 100644 index 00000000000..7990d84bad5 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_factory.h @@ -0,0 +1,35 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class AccountConsistencyModeManagerFactory + : public BrowserContextKeyedServiceFactory { + public: + // Returns an instance of the factory singleton. + static AccountConsistencyModeManagerFactory* GetInstance(); + + static AccountConsistencyModeManager* GetForProfile(Profile* profile); + + private: + friend struct base::DefaultSingletonTraits< + AccountConsistencyModeManagerFactory>; + + AccountConsistencyModeManagerFactory(); + ~AccountConsistencyModeManagerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + bool ServiceIsCreatedWithBrowserContext() const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc b/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc new file mode 100644 index 00000000000..d0a4ee97b83 --- /dev/null +++ b/chromium/chrome/browser/signin/account_consistency_mode_manager_unittest.cc @@ -0,0 +1,259 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/account_consistency_mode_manager.h" + +#include <memory> +#include <utility> + +#include "base/command_line.h" +#include "base/test/scoped_command_line.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/prefs/browser_prefs.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_profile.h" +#include "components/prefs/pref_notifier_impl.h" +#include "components/prefs/testing_pref_store.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +std::unique_ptr<TestingProfile> BuildTestingProfile(bool is_new_profile) { + TestingProfile::Builder profile_builder; + profile_builder.SetIsNewProfile(is_new_profile); + std::unique_ptr<TestingProfile> profile = profile_builder.Build(); + EXPECT_EQ(is_new_profile, profile->IsNewProfile()); + return profile; +} + +} // namespace + +// Check the default account consistency method. +TEST(AccountConsistencyModeManagerTest, DefaultValue) { + content::BrowserTaskEnvironment task_environment; + std::unique_ptr<TestingProfile> profile = + BuildTestingProfile(/*is_new_profile=*/false); + + signin::AccountConsistencyMethod method = +#if BUILDFLAG(ENABLE_MIRROR) + signin::AccountConsistencyMethod::kMirror; +#elif BUILDFLAG(ENABLE_DICE_SUPPORT) + signin::AccountConsistencyMethod::kDice; +#else +#error Either Dice or Mirror should be enabled +#endif + + EXPECT_EQ(method, + AccountConsistencyModeManager::GetMethodForProfile(profile.get())); + EXPECT_EQ( + method == signin::AccountConsistencyMethod::kMirror, + AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile.get())); + EXPECT_EQ( + method == signin::AccountConsistencyMethod::kDice, + AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get())); +} + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +// Checks that changing the signin-allowed pref changes the Dice state on next +// startup. +TEST(AccountConsistencyModeManagerTest, SigninAllowedChangesDiceState) { + content::BrowserTaskEnvironment task_environment; + std::unique_ptr<TestingProfile> profile = + BuildTestingProfile(/*is_new_profile=*/false); + + { + // First startup. + AccountConsistencyModeManager manager(profile.get()); + EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_TRUE( + profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); + + // User changes their settings. + profile->GetPrefs()->SetBoolean(prefs::kSigninAllowedOnNextStartup, false); + // Dice should remain in the same state until restart. + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); + } + + { + // Second startup. + AccountConsistencyModeManager manager(profile.get()); + // The signin-allowed pref should be disabled. + EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_FALSE( + profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup)); + // Dice should be disabled. + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + manager.GetAccountConsistencyMethod()); + } +} + +TEST(AccountConsistencyModeManagerTest, AllowBrowserSigninSwitch) { + content::BrowserTaskEnvironment task_environment; + std::unique_ptr<TestingProfile> profile = + BuildTestingProfile(/*is_new_profile=*/false); + { + base::test::ScopedCommandLine scoped_command_line; + scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII( + "allow-browser-signin", "false"); + AccountConsistencyModeManager manager(profile.get()); + EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + // Dice should be disabled. + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + manager.GetAccountConsistencyMethod()); + } + + { + base::test::ScopedCommandLine scoped_command_line; + scoped_command_line.GetProcessCommandLine()->AppendSwitchASCII( + "allow-browser-signin", "true"); + AccountConsistencyModeManager manager(profile.get()); + EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + // Dice should be enabled. + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); + } + + { + AccountConsistencyModeManager manager(profile.get()); + EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_TRUE( + profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup)); + // Dice should be enabled. + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); + } +} + +// Checks that Dice is enabled for new profiles. +TEST(AccountConsistencyModeManagerTest, DiceEnabledForNewProfiles) { + content::BrowserTaskEnvironment task_environment; + std::unique_ptr<TestingProfile> profile = + BuildTestingProfile(/*is_new_profile=*/false); + AccountConsistencyModeManager manager(profile.get()); + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + manager.GetAccountConsistencyMethod()); +} + +TEST(AccountConsistencyModeManagerTest, DiceOnlyForRegularProfile) { + content::BrowserTaskEnvironment task_environment; + + { + // Regular profile. + TestingProfile profile; + EXPECT_TRUE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDice, + AccountConsistencyModeManager::GetMethodForProfile(&profile)); + EXPECT_TRUE( + AccountConsistencyModeManager::ShouldBuildServiceForProfile(&profile)); + + // Incognito profile. + Profile* incognito_profile = + profile.GetPrimaryOTRProfile(/*create_if_needed=*/true); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + incognito_profile)); + EXPECT_FALSE( + AccountConsistencyModeManager::GetForProfile(incognito_profile)); + EXPECT_EQ( + signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(incognito_profile)); + EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile( + incognito_profile)); + + // Non-primary off-the-record profile. + Profile* otr_profile = profile.GetOffTheRecordProfile( + Profile::OTRProfileID::CreateUniqueForTesting(), + /*create_if_needed=*/true); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(otr_profile)); + EXPECT_FALSE(AccountConsistencyModeManager::GetForProfile(otr_profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(otr_profile)); + EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile( + otr_profile)); + } + + // Guest profile. + { + TestingProfile::Builder profile_builder; + profile_builder.SetGuestSession(); + std::unique_ptr<Profile> profile = profile_builder.Build(); + ASSERT_TRUE(profile->IsGuestSession()); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get())); + EXPECT_EQ( + signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(profile.get())); + EXPECT_FALSE(AccountConsistencyModeManager::ShouldBuildServiceForProfile( + profile.get())); + } +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(ENABLE_MIRROR) +// Mirror is enabled by default on Chrome OS, unless specified otherwise. +TEST(AccountConsistencyModeManagerTest, MirrorEnabledByDefault) { + // Creation of this object sets the current thread's id as UI thread. + content::BrowserTaskEnvironment task_environment; + + TestingProfile profile; + EXPECT_TRUE( + AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile)); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kMirror, + AccountConsistencyModeManager::GetMethodForProfile(&profile)); +} + +TEST(AccountConsistencyModeManagerTest, MirrorDisabledForGuestSession) { + // Creation of this object sets the current thread's id as UI thread. + content::BrowserTaskEnvironment task_environment; + + TestingProfile profile; + profile.SetGuestSession(true); + EXPECT_FALSE( + AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile)); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(&profile)); +} + +TEST(AccountConsistencyModeManagerTest, MirrorDisabledForOffTheRecordProfile) { + // Creation of this object sets the current thread's id as UI thread. + content::BrowserTaskEnvironment task_environment; + + TestingProfile profile; + Profile* incognito_profile = + profile.GetPrimaryOTRProfile(/*create_if_needed=*/true); + EXPECT_FALSE(AccountConsistencyModeManager::IsMirrorEnabledForProfile( + incognito_profile)); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + incognito_profile)); + EXPECT_EQ( + signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(incognito_profile)); + + Profile* otr_profile = profile.GetOffTheRecordProfile( + Profile::OTRProfileID::CreateUniqueForTesting(), + /*create_if_needed=*/true); + EXPECT_FALSE( + AccountConsistencyModeManager::IsMirrorEnabledForProfile(otr_profile)); + EXPECT_FALSE( + AccountConsistencyModeManager::IsDiceEnabledForProfile(otr_profile)); + EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled, + AccountConsistencyModeManager::GetMethodForProfile(otr_profile)); +} + +#endif // BUILDFLAG(ENABLE_MIRROR) diff --git a/chromium/chrome/browser/signin/account_id_from_account_info.cc b/chromium/chrome/browser/signin/account_id_from_account_info.cc new file mode 100644 index 00000000000..c801c7ac983 --- /dev/null +++ b/chromium/chrome/browser/signin/account_id_from_account_info.cc @@ -0,0 +1,24 @@ +// 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. + +#include "chrome/browser/signin/account_id_from_account_info.h" +#include "build/chromeos_buildflags.h" +#include "google_apis/gaia/gaia_auth_util.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "components/user_manager/known_user.h" +#endif + +AccountId AccountIdFromAccountInfo(const CoreAccountInfo& account_info) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + return user_manager::known_user::GetAccountId( + account_info.email, account_info.gaia, AccountType::GOOGLE); +#else + if (account_info.email.empty() || account_info.gaia.empty()) + return EmptyAccountId(); + + return AccountId::FromUserEmailGaiaId( + gaia::CanonicalizeEmail(account_info.email), account_info.gaia); +#endif +} diff --git a/chromium/chrome/browser/signin/account_id_from_account_info.h b/chromium/chrome/browser/signin/account_id_from_account_info.h new file mode 100644 index 00000000000..cdd6836878d --- /dev/null +++ b/chromium/chrome/browser/signin/account_id_from_account_info.h @@ -0,0 +1,18 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_ + +#include "components/account_id/account_id.h" +#include "components/signin/public/identity_manager/account_info.h" + +// Returns AccountID populated from |account_info|. +// NOTE: This utility is in //chrome rather than being part of +// //components/signin/public because it is only //chrome that needs to go back +// and forth between AccountId and AccountInfo, and it is outside the scope of +// //components/signin/public to have knowledge about AccountId. +AccountId AccountIdFromAccountInfo(const CoreAccountInfo& account_info); + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_ID_FROM_ACCOUNT_INFO_H_ diff --git a/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc b/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc new file mode 100644 index 00000000000..356e3385e88 --- /dev/null +++ b/chromium/chrome/browser/signin/account_id_from_account_info_unittest.cc @@ -0,0 +1,20 @@ +// 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. + +#include "chrome/browser/signin/account_id_from_account_info.h" +#include "testing/gtest/include/gtest/gtest.h" + +class AccountIdFromAccountInfoTest : public testing::Test {}; + +// Tests that AccountIdFromAccountInfo() passes along a canonicalized email to +// AccountId. +TEST(AccountIdFromAccountInfoTest, + AccountIdFromAccountInfo_CanonicalizesRawEmail) { + AccountInfo info; + info.email = "test.email@gmail.com"; + info.gaia = "test_id"; + + EXPECT_EQ("testemail@gmail.com", + AccountIdFromAccountInfo(info).GetUserEmail()); +} diff --git a/chromium/chrome/browser/signin/account_investigator_factory.cc b/chromium/chrome/browser/signin/account_investigator_factory.cc new file mode 100644 index 00000000000..731c8b45a8b --- /dev/null +++ b/chromium/chrome/browser/signin/account_investigator_factory.cc @@ -0,0 +1,57 @@ +// Copyright 2016 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. + +#include "chrome/browser/signin/account_investigator_factory.h" + +#include "base/memory/singleton.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service_factory.h" +#include "components/signin/core/browser/account_investigator.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +// static +AccountInvestigatorFactory* AccountInvestigatorFactory::GetInstance() { + return base::Singleton<AccountInvestigatorFactory>::get(); +} + +// static +AccountInvestigator* AccountInvestigatorFactory::GetForProfile( + Profile* profile) { + return static_cast<AccountInvestigator*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +AccountInvestigatorFactory::AccountInvestigatorFactory() + : BrowserContextKeyedServiceFactory( + "AccountInvestigator", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +AccountInvestigatorFactory::~AccountInvestigatorFactory() {} + +KeyedService* AccountInvestigatorFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile(Profile::FromBrowserContext(context)); + AccountInvestigator* investigator = new AccountInvestigator( + profile->GetPrefs(), IdentityManagerFactory::GetForProfile(profile)); + investigator->Initialize(); + return investigator; +} + +void AccountInvestigatorFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + AccountInvestigator::RegisterPrefs(registry); +} + +bool AccountInvestigatorFactory::ServiceIsCreatedWithBrowserContext() const { + return true; +} + +bool AccountInvestigatorFactory::ServiceIsNULLWhileTesting() const { + return true; +} diff --git a/chromium/chrome/browser/signin/account_investigator_factory.h b/chromium/chrome/browser/signin/account_investigator_factory.h new file mode 100644 index 00000000000..6f1d56caec5 --- /dev/null +++ b/chromium/chrome/browser/signin/account_investigator_factory.h @@ -0,0 +1,44 @@ +// Copyright 2016 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. + +#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_ + +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class Profile; +class AccountInvestigator; + +namespace base { +template <typename T> +struct DefaultSingletonTraits; +} // namespace base + +// Factory for BrowserKeyedService AccountInvestigator. +class AccountInvestigatorFactory : public BrowserContextKeyedServiceFactory { + public: + static AccountInvestigator* GetForProfile(Profile* profile); + + static AccountInvestigatorFactory* GetInstance(); + + AccountInvestigatorFactory(const AccountInvestigatorFactory&) = delete; + AccountInvestigatorFactory& operator=(const AccountInvestigatorFactory&) = + delete; + + private: + friend struct base::DefaultSingletonTraits<AccountInvestigatorFactory>; + + AccountInvestigatorFactory(); + ~AccountInvestigatorFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + bool ServiceIsCreatedWithBrowserContext() const override; + bool ServiceIsNULLWhileTesting() const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_INVESTIGATOR_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/account_reconcilor_factory.cc b/chromium/chrome/browser/signin/account_reconcilor_factory.cc new file mode 100644 index 00000000000..9b47faae104 --- /dev/null +++ b/chromium/chrome/browser/signin/account_reconcilor_factory.cc @@ -0,0 +1,212 @@ +// Copyright 2013 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. + +#include "chrome/browser/signin/account_reconcilor_factory.h" + +#include <memory> +#include <utility> + +#include "base/check.h" +#include "base/feature_list.h" +#include "base/notreached.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/account_reconcilor_delegate.h" +#include "components/signin/core/browser/mirror_account_reconcilor_delegate.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_buildflags.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "base/metrics/histogram_macros.h" +#include "base/time/time.h" +#include "chrome/browser/ash/account_manager/account_manager_util.h" +#include "chrome/browser/lifetime/application_lifetime.h" +#include "chromeos/tpm/install_attributes.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/active_directory_account_reconcilor_delegate.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/user_manager/user_manager.h" +#include "google_apis/gaia/google_service_auth_error.h" +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "components/signin/core/browser/dice_account_reconcilor_delegate.h" +#endif + +namespace { + +#if BUILDFLAG(IS_CHROMEOS_ASH) +class ChromeOSLimitedAccessAccountReconcilorDelegate + : public signin::MirrorAccountReconcilorDelegate { + public: + enum class ReconcilorBehavior { + kChild, + kEnterprise, + }; + + ChromeOSLimitedAccessAccountReconcilorDelegate( + ReconcilorBehavior reconcilor_behavior, + signin::IdentityManager* identity_manager) + : signin::MirrorAccountReconcilorDelegate(identity_manager), + reconcilor_behavior_(reconcilor_behavior) {} + + ChromeOSLimitedAccessAccountReconcilorDelegate( + const ChromeOSLimitedAccessAccountReconcilorDelegate&) = delete; + ChromeOSLimitedAccessAccountReconcilorDelegate& operator=( + const ChromeOSLimitedAccessAccountReconcilorDelegate&) = delete; + + base::TimeDelta GetReconcileTimeout() const override { + switch (reconcilor_behavior_) { + case ReconcilorBehavior::kChild: + return base::Seconds(10); + case ReconcilorBehavior::kEnterprise: + // 60 seconds is enough to cover about 99% of all reconcile cases. + return base::Seconds(60); + default: + NOTREACHED(); + return MirrorAccountReconcilorDelegate::GetReconcileTimeout(); + } + } + + void OnReconcileError(const GoogleServiceAuthError& error) override { + // If |error| is |GoogleServiceAuthError::State::NONE| or a transient error. + if (!error.IsPersistentError()) { + return; + } + + if (!GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSignin))) { + return; + } + + // Mark the account to require an online sign in. + const user_manager::User* primary_user = + user_manager::UserManager::Get()->GetPrimaryUser(); + DCHECK(primary_user); + user_manager::UserManager::Get()->SaveForceOnlineSignin( + primary_user->GetAccountId(), true /* force_online_signin */); + + if (reconcilor_behavior_ == ReconcilorBehavior::kChild) { + UMA_HISTOGRAM_BOOLEAN( + "ChildAccountReconcilor.ForcedUserExitOnReconcileError", true); + } + // Force a logout. + chrome::AttemptUserExit(); + } + + private: + const ReconcilorBehavior reconcilor_behavior_; +}; +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + +} // namespace + +AccountReconcilorFactory::AccountReconcilorFactory() + : BrowserContextKeyedServiceFactory( + "AccountReconcilor", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(ChromeSigninClientFactory::GetInstance()); + DependsOn(IdentityManagerFactory::GetInstance()); +} + +AccountReconcilorFactory::~AccountReconcilorFactory() {} + +// static +AccountReconcilor* AccountReconcilorFactory::GetForProfile(Profile* profile) { + return static_cast<AccountReconcilor*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +AccountReconcilorFactory* AccountReconcilorFactory::GetInstance() { + return base::Singleton<AccountReconcilorFactory>::get(); +} + +KeyedService* AccountReconcilorFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + SigninClient* signin_client = + ChromeSigninClientFactory::GetForProfile(profile); + AccountReconcilor* reconcilor = + new AccountReconcilor(identity_manager, signin_client, + CreateAccountReconcilorDelegate(profile)); + reconcilor->Initialize(true /* start_reconcile_if_tokens_available */); + return reconcilor; +} + +void AccountReconcilorFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + registry->RegisterBooleanPref(prefs::kForceLogoutUnauthenticatedUserEnabled, + false); +#endif +} + +// static +std::unique_ptr<signin::AccountReconcilorDelegate> +AccountReconcilorFactory::CreateAccountReconcilorDelegate(Profile* profile) { + signin::AccountConsistencyMethod account_consistency = + AccountConsistencyModeManager::GetMethodForProfile(profile); + switch (account_consistency) { + case signin::AccountConsistencyMethod::kMirror: +#if BUILDFLAG(IS_CHROMEOS_ASH) + // Only for child accounts on Chrome OS, use the specialized Mirror + // delegate. + if (profile->IsChild()) { + return std::make_unique<ChromeOSLimitedAccessAccountReconcilorDelegate>( + ChromeOSLimitedAccessAccountReconcilorDelegate::ReconcilorBehavior:: + kChild, + IdentityManagerFactory::GetForProfile(profile)); + } + + // Only for Active Directory accounts on Chrome OS. + // TODO(https://crbug.com/993317): Remove the check for + // |IsAccountManagerAvailable| after fixing https://crbug.com/1008349 and + // https://crbug.com/993317. + if (ash::IsAccountManagerAvailable(profile) && + chromeos::InstallAttributes::Get()->IsActiveDirectoryManaged()) { + return std::make_unique< + signin::ActiveDirectoryAccountReconcilorDelegate>(); + } + + if (profile->GetPrefs()->GetBoolean( + prefs::kForceLogoutUnauthenticatedUserEnabled)) { + return std::make_unique<ChromeOSLimitedAccessAccountReconcilorDelegate>( + ChromeOSLimitedAccessAccountReconcilorDelegate::ReconcilorBehavior:: + kEnterprise, + IdentityManagerFactory::GetForProfile(profile)); + } + + return std::make_unique<signin::MirrorAccountReconcilorDelegate>( + IdentityManagerFactory::GetForProfile(profile)); +#else + return std::make_unique<signin::MirrorAccountReconcilorDelegate>( + IdentityManagerFactory::GetForProfile(profile)); +#endif + + case signin::AccountConsistencyMethod::kDisabled: + return std::make_unique<signin::AccountReconcilorDelegate>(); + + case signin::AccountConsistencyMethod::kDice: +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + return std::make_unique<signin::DiceAccountReconcilorDelegate>(); +#else + NOTREACHED(); + return nullptr; +#endif + } + + NOTREACHED(); + return nullptr; +} diff --git a/chromium/chrome/browser/signin/account_reconcilor_factory.h b/chromium/chrome/browser/signin/account_reconcilor_factory.h new file mode 100644 index 00000000000..1358fa13cac --- /dev/null +++ b/chromium/chrome/browser/signin/account_reconcilor_factory.h @@ -0,0 +1,57 @@ +// Copyright 2013 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. + +#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_ + +#include <memory> + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace signin { +class IdentityManager; +} + +namespace signin { +class AccountReconcilorDelegate; +} + +class AccountReconcilor; +class Profile; +class SigninClient; + +// Singleton that owns all AccountReconcilors and associates them with +// Profiles. Listens for the Profile's destruction notification and cleans up. +class AccountReconcilorFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of AccountReconcilor associated with this profile + // (creating one if none exists). Returns NULL if this profile cannot have an + // AccountReconcilor (for example, if |profile| is incognito). + static AccountReconcilor* GetForProfile(Profile* profile); + + // Returns an instance of the factory singleton. + static AccountReconcilorFactory* GetInstance(); + + // BrowserContextKeyedServiceFactory: + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + + private: + friend struct base::DefaultSingletonTraits<AccountReconcilorFactory>; + friend class DummyAccountReconcilorWithDelegate; // For testing. + + AccountReconcilorFactory(); + ~AccountReconcilorFactory() override; + + // Creates the AccountReconcilorDelegate. + static std::unique_ptr<signin::AccountReconcilorDelegate> + CreateAccountReconcilorDelegate(Profile* profile); + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_ACCOUNT_RECONCILOR_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper.cc b/chromium/chrome/browser/signin/chrome_device_id_helper.cc new file mode 100644 index 00000000000..7b28246d24a --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_device_id_helper.cc @@ -0,0 +1,87 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/chrome_device_id_helper.h" + +#include <string> + +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "components/signin/public/base/device_id_helper.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "base/command_line.h" +#include "base/guid.h" +#include "base/logging.h" +#include "chrome/browser/ash/profiles/profile_helper.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/base/signin_switches.h" +#include "components/user_manager/known_user.h" +#include "components/user_manager/user_manager.h" +#endif + +std::string GetSigninScopedDeviceIdForProfile(Profile* profile) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kDisableSigninScopedDeviceId)) { + return std::string(); + } + + // UserManager may not exist in unit_tests. + if (!user_manager::UserManager::IsInitialized()) + return std::string(); + + const user_manager::User* user = + chromeos::ProfileHelper::Get()->GetUserByProfile(profile); + if (!user) + return std::string(); + + const std::string signin_scoped_device_id = + user_manager::known_user::GetDeviceId(user->GetAccountId()); + LOG_IF(ERROR, signin_scoped_device_id.empty()) + << "Device ID is not set for user."; + return signin_scoped_device_id; +#else + return signin::GetSigninScopedDeviceId(profile->GetPrefs()); +#endif +} + +#if BUILDFLAG(IS_CHROMEOS_ASH) + +std::string GenerateSigninScopedDeviceId(bool for_ephemeral) { + constexpr char kEphemeralUserDeviceIDPrefix[] = "t_"; + std::string guid = base::GenerateGUID(); + return for_ephemeral ? kEphemeralUserDeviceIDPrefix + guid : guid; +} + +void MigrateSigninScopedDeviceId(Profile* profile) { + // UserManager may not exist in unit_tests. + if (!user_manager::UserManager::IsInitialized()) + return; + + const user_manager::User* user = + chromeos::ProfileHelper::Get()->GetUserByProfile(profile); + if (!user) + return; + const AccountId account_id = user->GetAccountId(); + if (user_manager::known_user::GetDeviceId(account_id).empty()) { + const std::string legacy_device_id = profile->GetPrefs()->GetString( + prefs::kGoogleServicesSigninScopedDeviceId); + if (!legacy_device_id.empty()) { + // Need to move device ID from the old location to the new one, if it has + // not been done yet. + user_manager::known_user::SetDeviceId(account_id, legacy_device_id); + } else { + user_manager::known_user::SetDeviceId( + account_id, GenerateSigninScopedDeviceId( + user_manager::UserManager::Get() + ->IsUserNonCryptohomeDataEphemeral(account_id))); + } + } + profile->GetPrefs()->SetString(prefs::kGoogleServicesSigninScopedDeviceId, + std::string()); +} + +#endif diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper.h b/chromium/chrome/browser/signin/chrome_device_id_helper.h new file mode 100644 index 00000000000..9e84d416528 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_device_id_helper.h @@ -0,0 +1,36 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_ + +#include <string> + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" + +class Profile; + +// Returns the device ID that is scoped to single signin. +// All refresh tokens for |profile| are annotated with this device ID when they +// are requested. +// On non-ChromeOS platforms, this is equivalent to: +// signin::GetSigninScopedDeviceId(profile->GetPrefs()); +std::string GetSigninScopedDeviceIdForProfile(Profile* profile); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + +// Helper method. The device ID should generally be obtained through +// GetSigninScopedDeviceIdForProfile(). +// If |for_ephemeral| is true, special kind of device ID for ephemeral users is +// generated. +std::string GenerateSigninScopedDeviceId(bool for_ephemeral); + +// Moves any existing device ID out of the pref service into the UserManager, +// and creates a new ID if it is empty. +void MigrateSigninScopedDeviceId(Profile* profile); + +#endif + +#endif // CHROME_BROWSER_SIGNIN_CHROME_DEVICE_ID_HELPER_H_ diff --git a/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc b/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc new file mode 100644 index 00000000000..3e2f2e9517b --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_device_id_helper_unittest.cc @@ -0,0 +1,40 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/chrome_device_id_helper.h" + +#include <string> + +#include "base/strings/string_util.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +const char kEpehemeralPrefix[] = "t_"; + +TEST(DeviceIdHelper, NotEphemeral) { + std::string device_id = + GenerateSigninScopedDeviceId(false /* for_ephemeral */); + // Not empty. + EXPECT_FALSE(device_id.empty()); + // No ephemeral prefix. + EXPECT_FALSE(base::StartsWith(device_id, kEpehemeralPrefix, + base::CompareCase::SENSITIVE)); + // ID is unique. + EXPECT_NE(device_id, GenerateSigninScopedDeviceId(false /* for_ephemeral */)); +} + +TEST(DeviceIdHelper, Ephemeral) { + std::string device_id = + GenerateSigninScopedDeviceId(true /* for_ephemeral */); + // Ephemeral prefix. + EXPECT_TRUE(base::StartsWith(device_id, kEpehemeralPrefix, + base::CompareCase::SENSITIVE)); + // Not empty. + EXPECT_NE(device_id, kEpehemeralPrefix); + // ID is unique. + EXPECT_NE(device_id, GenerateSigninScopedDeviceId(true /* for_ephemeral */)); +} +#endif diff --git a/chromium/chrome/browser/signin/chrome_signin_client.cc b/chromium/chrome/browser/signin/chrome_signin_client.cc new file mode 100644 index 00000000000..3a5ea106adb --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client.cc @@ -0,0 +1,369 @@ +// Copyright 2014 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. + +#include "chrome/browser/signin/chrome_signin_client.h" + +#include <stddef.h> + +#include <memory> +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/content_settings/cookie_settings_factory.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" +#include "chrome/browser/enterprise/util/managed_browser_utils.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_metrics.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/chrome_device_id_helper.h" +#include "chrome/browser/signin/force_signin_verifier.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/common/buildflags.h" +#include "chrome/common/channel_info.h" +#include "chrome/common/pref_names.h" +#include "components/content_settings/core/browser/cookie_settings.h" +#include "components/metrics/metrics_service.h" +#include "components/policy/core/browser/browser_policy_connector.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/cookie_settings_util.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/access_token_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/scope_set.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/browser/storage_partition.h" +#include "google_apis/gaia/gaia_constants.h" +#include "google_apis/gaia/gaia_urls.h" +#include "url/gurl.h" + +#if BUILDFLAG(ENABLE_SUPERVISED_USERS) +#include "chrome/browser/supervised_user/supervised_user_constants.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/net/delay_network_call.h" +#include "chromeos/network/network_handler.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "chrome/browser/lacros/account_manager/account_manager_util.h" +#include "chromeos/crosapi/mojom/account_manager.mojom.h" +#include "chromeos/lacros/lacros_service.h" +#include "components/account_manager_core/account.h" +#include "components/account_manager_core/account_manager_util.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#endif + +#if !defined(OS_ANDROID) +#include "chrome/browser/profiles/profile_window.h" +#endif + +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/profile_picker.h" +#endif + +namespace { + +// List of sources for which sign out is always allowed. +signin_metrics::ProfileSignout kAlwaysAllowedSignoutSources[] = { + // Allowed, because data has not been synced yet. + signin_metrics::ProfileSignout::ABORT_SIGNIN, + // Allowed, because only used on Android and the primary account must be + // cleared when the account is removed from device + signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE, + // Allowed to force finish the account id migration. + signin_metrics::ACCOUNT_ID_MIGRATION, + // Allowed, for tests. + signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST}; + +SigninClient::SignoutDecision IsSignoutAllowed( + Profile* profile, + const signin_metrics::ProfileSignout signout_source) { + if (signin_util::IsUserSignoutAllowedForProfile(profile)) + return SigninClient::SignoutDecision::ALLOW_SIGNOUT; + + auto* identity_manager = + IdentityManagerFactory::GetForProfileIfExists(profile); + if (identity_manager && + !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + return SigninClient::SignoutDecision::ALLOW_SIGNOUT; + } + + for (const auto& always_allowed_source : kAlwaysAllowedSignoutSources) { + if (signout_source == always_allowed_source) + return SigninClient::SignoutDecision::ALLOW_SIGNOUT; + } + + return SigninClient::SignoutDecision::DISALLOW_SIGNOUT; +} + +} // namespace + +ChromeSigninClient::ChromeSigninClient(Profile* profile) : profile_(profile) { +#if !BUILDFLAG(IS_CHROMEOS_ASH) + content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this); +#endif +} + +ChromeSigninClient::~ChromeSigninClient() { +#if !BUILDFLAG(IS_CHROMEOS_ASH) + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this); +#endif +} + +void ChromeSigninClient::DoFinalInit() { + VerifySyncToken(); +} + +// static +bool ChromeSigninClient::ProfileAllowsSigninCookies(Profile* profile) { + content_settings::CookieSettings* cookie_settings = + CookieSettingsFactory::GetForProfile(profile).get(); + return signin::SettingsAllowSigninCookies(cookie_settings); +} + +PrefService* ChromeSigninClient::GetPrefs() { return profile_->GetPrefs(); } + +scoped_refptr<network::SharedURLLoaderFactory> +ChromeSigninClient::GetURLLoaderFactory() { + if (url_loader_factory_for_testing_) + return url_loader_factory_for_testing_; + + return profile_->GetDefaultStoragePartition() + ->GetURLLoaderFactoryForBrowserProcess(); +} + +network::mojom::CookieManager* ChromeSigninClient::GetCookieManager() { + return profile_->GetDefaultStoragePartition() + ->GetCookieManagerForBrowserProcess(); +} + +bool ChromeSigninClient::AreSigninCookiesAllowed() { + return ProfileAllowsSigninCookies(profile_); +} + +bool ChromeSigninClient::AreSigninCookiesDeletedOnExit() { + content_settings::CookieSettings* cookie_settings = + CookieSettingsFactory::GetForProfile(profile_).get(); + return signin::SettingsDeleteSigninCookiesOnExit(cookie_settings); +} + +void ChromeSigninClient::AddContentSettingsObserver( + content_settings::Observer* observer) { + HostContentSettingsMapFactory::GetForProfile(profile_) + ->AddObserver(observer); +} + +void ChromeSigninClient::RemoveContentSettingsObserver( + content_settings::Observer* observer) { + HostContentSettingsMapFactory::GetForProfile(profile_) + ->RemoveObserver(observer); +} + +void ChromeSigninClient::PreSignOut( + base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached, + signin_metrics::ProfileSignout signout_source_metric) { + DCHECK(on_signout_decision_reached); + DCHECK(!on_signout_decision_reached_) << "SignOut already in-progress!"; + on_signout_decision_reached_ = std::move(on_signout_decision_reached); + +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + // `signout_source_metric` is `signin_metrics::ABORT_SIGNIN` if the user + // declines sync in the signin process. In case the user accepts the managed + // account but declines sync, we should keep the window open. + bool user_declines_sync_after_consenting_to_management = + signout_source_metric == signin_metrics::ABORT_SIGNIN && + chrome::enterprise_util::UserAcceptedAccountManagement(profile_); + // These sign out won't remove the policy cache, keep the window opened. + bool keep_window_opened = + signout_source_metric == + signin_metrics::GOOGLE_SERVICE_NAME_PATTERN_CHANGED || + signout_source_metric == signin_metrics::SERVER_FORCED_DISABLE || + signout_source_metric == signin_metrics::SIGNOUT_PREF_CHANGED || + user_declines_sync_after_consenting_to_management; + if (signin_util::IsForceSigninEnabled() && !profile_->IsSystemProfile() && + !profile_->IsGuestSession() && !profile_->IsChild() && + !keep_window_opened) { + if (signout_source_metric == + signin_metrics::SIGNIN_PREF_CHANGED_DURING_SIGNIN) { + // SIGNIN_PREF_CHANGED_DURING_SIGNIN will be triggered when + // IdentityManager is initialized before window opening, there is no need + // to close window. Call OnCloseBrowsersSuccess to continue sign out and + // show UserManager afterwards. + should_display_user_manager_ = false; // Don't show UserManager twice. + OnCloseBrowsersSuccess(signout_source_metric, profile_->GetPath()); + } else { + BrowserList::CloseAllBrowsersWithProfile( + profile_, + base::BindRepeating(&ChromeSigninClient::OnCloseBrowsersSuccess, + base::Unretained(this), signout_source_metric), + base::BindRepeating(&ChromeSigninClient::OnCloseBrowsersAborted, + base::Unretained(this)), + signout_source_metric == signin_metrics::ABORT_SIGNIN || + signout_source_metric == + signin_metrics::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN || + signout_source_metric == signin_metrics::TRANSFER_CREDENTIALS); + } + } else { +#else + { +#endif + std::move(on_signout_decision_reached_) + .Run(IsSignoutAllowed(profile_, signout_source_metric)); + } +} + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +void ChromeSigninClient::OnConnectionChanged( + network::mojom::ConnectionType type) { + if (type == network::mojom::ConnectionType::CONNECTION_NONE) + return; + + for (base::OnceClosure& callback : delayed_callbacks_) + std::move(callback).Run(); + + delayed_callbacks_.clear(); +} +#endif + +void ChromeSigninClient::DelayNetworkCall(base::OnceClosure callback) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + // Do not make network requests in unit tests. chromeos::NetworkHandler should + // not be used and is not expected to have been initialized in unit tests. + if (url_loader_factory_for_testing_ && + !chromeos::NetworkHandler::IsInitialized()) { + std::move(callback).Run(); + return; + } + chromeos::DelayNetworkCall( + base::Milliseconds(chromeos::kDefaultNetworkRetryDelayMS), + std::move(callback)); + return; +#else + // Don't bother if we don't have any kind of network connection. + network::mojom::ConnectionType type; + bool sync = content::GetNetworkConnectionTracker()->GetConnectionType( + &type, base::BindOnce(&ChromeSigninClient::OnConnectionChanged, + weak_ptr_factory_.GetWeakPtr())); + if (!sync || type == network::mojom::ConnectionType::CONNECTION_NONE) { + // Connection type cannot be retrieved synchronously so delay the callback. + delayed_callbacks_.push_back(std::move(callback)); + } else { + std::move(callback).Run(); + } +#endif +} + +std::unique_ptr<GaiaAuthFetcher> ChromeSigninClient::CreateGaiaAuthFetcher( + GaiaAuthConsumer* consumer, + gaia::GaiaSource source) { + return std::make_unique<GaiaAuthFetcher>(consumer, source, + GetURLLoaderFactory()); +} + +void ChromeSigninClient::VerifySyncToken() { +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + // We only verifiy the token once when Profile is just created. + if (signin_util::IsForceSigninEnabled() && !force_signin_verifier_) + force_signin_verifier_ = std::make_unique<ForceSigninVerifier>( + profile_, IdentityManagerFactory::GetForProfile(profile_)); +#endif +} + +bool ChromeSigninClient::IsNonEnterpriseUser(const std::string& username) { + return policy::BrowserPolicyConnector::IsNonEnterpriseUser(username); +} + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +// Returns the account that must be auto-signed-in to the Main Profile in +// Lacros. +// This is, when available, the account used to sign into the Chrome OS +// session. This may be a Gaia account or a Microsoft Active Directory +// account. This field will be null for Guest sessions, Managed Guest +// sessions, Demo mode, and Kiosks. Note that this is different from the +// concept of a Primary Account in the browser. A user may not be signed into +// a Lacros browser Profile, or may be signed into a browser Profile with an +// account which is different from the account which they used to sign into +// the device - aka Device Account. +// Also note that this will be null for Secondary / non-Main Profiles in +// Lacros, because they do not start with the Chrome OS Device Account +// signed-in by default. +absl::optional<account_manager::Account> +ChromeSigninClient::GetInitialPrimaryAccount() { + if (!profile_->IsMainProfile()) + return absl::nullopt; + + const crosapi::mojom::AccountPtr& device_account = + chromeos::LacrosService::Get()->init_params()->device_account; + if (!device_account) + return absl::nullopt; + + return account_manager::FromMojoAccount(device_account); +} +#endif + +void ChromeSigninClient::SetURLLoaderFactoryForTest( + scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) { + url_loader_factory_for_testing_ = url_loader_factory; +} + +void ChromeSigninClient::OnCloseBrowsersSuccess( + const signin_metrics::ProfileSignout signout_source_metric, + const base::FilePath& profile_path) { +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + if (signin_util::IsForceSigninEnabled() && force_signin_verifier_.get()) { + force_signin_verifier_->Cancel(); + } +#endif + + std::move(on_signout_decision_reached_) + .Run(IsSignoutAllowed(profile_, signout_source_metric)); + + LockForceSigninProfile(profile_path); + // After sign out, lock the profile and show UserManager if necessary. + if (should_display_user_manager_) { + ShowUserManager(profile_path); + } else { + should_display_user_manager_ = true; + } +} + +void ChromeSigninClient::OnCloseBrowsersAborted( + const base::FilePath& profile_path) { + should_display_user_manager_ = true; + + // Disallow sign-out (aborted). + std::move(on_signout_decision_reached_) + .Run(SignoutDecision::DISALLOW_SIGNOUT); +} + +void ChromeSigninClient::LockForceSigninProfile( + const base::FilePath& profile_path) { + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile_->GetPath()); + if (!entry) + return; + entry->LockForceSigninProfile(true); +} + +void ChromeSigninClient::ShowUserManager(const base::FilePath& profile_path) { +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + ProfilePicker::Show(ProfilePicker::EntryPoint::kProfileLocked); +#endif +} diff --git a/chromium/chrome/browser/signin/chrome_signin_client.h b/chromium/chrome/browser/signin/chrome_signin_client.h new file mode 100644 index 00000000000..cef8fd3c32e --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client.h @@ -0,0 +1,115 @@ +// Copyright 2014 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. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_ + +#include <list> +#include <memory> +#include <string> + +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "components/signin/public/base/signin_client.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/mojom/network_change_manager.mojom-forward.h" + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +#include "services/network/public/cpp/network_connection_tracker.h" +#endif + +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) +class ForceSigninVerifier; +#endif +class Profile; + +class ChromeSigninClient + : public SigninClient +#if !BUILDFLAG(IS_CHROMEOS_ASH) + , + public network::NetworkConnectionTracker::NetworkConnectionObserver +#endif +{ + public: + explicit ChromeSigninClient(Profile* profile); + + ChromeSigninClient(const ChromeSigninClient&) = delete; + ChromeSigninClient& operator=(const ChromeSigninClient&) = delete; + + ~ChromeSigninClient() override; + + void DoFinalInit() override; + + // Utility method. + static bool ProfileAllowsSigninCookies(Profile* profile); + + // SigninClient implementation. + PrefService* GetPrefs() override; + void PreSignOut( + base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached, + signin_metrics::ProfileSignout signout_source_metric) override; + scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory() override; + network::mojom::CookieManager* GetCookieManager() override; + bool AreSigninCookiesAllowed() override; + bool AreSigninCookiesDeletedOnExit() override; + void AddContentSettingsObserver( + content_settings::Observer* observer) override; + void RemoveContentSettingsObserver( + content_settings::Observer* observer) override; + void DelayNetworkCall(base::OnceClosure callback) override; + std::unique_ptr<GaiaAuthFetcher> CreateGaiaAuthFetcher( + GaiaAuthConsumer* consumer, + gaia::GaiaSource source) override; + bool IsNonEnterpriseUser(const std::string& username) override; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) + // network::NetworkConnectionTracker::NetworkConnectionObserver + // implementation. + void OnConnectionChanged(network::mojom::ConnectionType type) override; +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) + absl::optional<account_manager::Account> GetInitialPrimaryAccount() override; +#endif + + // Used in tests to override the URLLoaderFactory returned by + // GetURLLoaderFactory(). + void SetURLLoaderFactoryForTest( + scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory); + + protected: + virtual void ShowUserManager(const base::FilePath& profile_path); + virtual void LockForceSigninProfile(const base::FilePath& profile_path); + + private: + void VerifySyncToken(); + void OnCloseBrowsersSuccess( + const signin_metrics::ProfileSignout signout_source_metric, + const base::FilePath& profile_path); + void OnCloseBrowsersAborted(const base::FilePath& profile_path); + + raw_ptr<Profile> profile_; + + // Stored callback from PreSignOut(); + base::OnceCallback<void(SignoutDecision)> on_signout_decision_reached_; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) + std::list<base::OnceClosure> delayed_callbacks_; +#endif + + bool should_display_user_manager_ = true; +#if !defined(OS_ANDROID) && !BUILDFLAG(IS_CHROMEOS_ASH) + std::unique_ptr<ForceSigninVerifier> force_signin_verifier_; +#endif + + scoped_refptr<network::SharedURLLoaderFactory> + url_loader_factory_for_testing_; + + base::WeakPtrFactory<ChromeSigninClient> weak_ptr_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_client_factory.cc b/chromium/chrome/browser/signin/chrome_signin_client_factory.cc new file mode 100644 index 00000000000..62c0d638cbd --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_factory.cc @@ -0,0 +1,34 @@ +// Copyright 2014 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. + +#include "chrome/browser/signin/chrome_signin_client_factory.h" + +#include "chrome/browser/net/profile_network_context_service_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +ChromeSigninClientFactory::ChromeSigninClientFactory() + : BrowserContextKeyedServiceFactory( + "ChromeSigninClient", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(ProfileNetworkContextServiceFactory::GetInstance()); +} + +ChromeSigninClientFactory::~ChromeSigninClientFactory() {} + +// static +SigninClient* ChromeSigninClientFactory::GetForProfile(Profile* profile) { + return static_cast<SigninClient*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +ChromeSigninClientFactory* ChromeSigninClientFactory::GetInstance() { + return base::Singleton<ChromeSigninClientFactory>::get(); +} + +KeyedService* ChromeSigninClientFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new ChromeSigninClient(Profile::FromBrowserContext(context)); +} diff --git a/chromium/chrome/browser/signin/chrome_signin_client_factory.h b/chromium/chrome/browser/signin/chrome_signin_client_factory.h new file mode 100644 index 00000000000..e88d3f23b00 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_factory.h @@ -0,0 +1,37 @@ +// Copyright 2014 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. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "chrome/browser/signin/chrome_signin_client.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class Profile; + +// Singleton that owns all ChromeSigninClients and associates them with +// Profiles. +class ChromeSigninClientFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of SigninClient associated with this profile + // (creating one if none exists). Returns NULL if this profile cannot have an + // SigninClient (for example, if |profile| is incognito). + static SigninClient* GetForProfile(Profile* profile); + + // Returns an instance of the factory singleton. + static ChromeSigninClientFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits<ChromeSigninClientFactory>; + + ChromeSigninClientFactory(); + ~ChromeSigninClientFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc b/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc new file mode 100644 index 00000000000..e0412419de1 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_test_util.cc @@ -0,0 +1,21 @@ +// Copyright 2016 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. + +#include "chrome/browser/signin/chrome_signin_client_test_util.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/chrome_signin_client.h" +#include "components/keyed_service/core/keyed_service.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_factory.h" + +std::unique_ptr<KeyedService> BuildChromeSigninClientWithURLLoader( + network::TestURLLoaderFactory* test_url_loader_factory, + content::BrowserContext* context) { + Profile* profile = Profile::FromBrowserContext(context); + auto signin_client = std::make_unique<ChromeSigninClient>(profile); + signin_client->SetURLLoaderFactoryForTest( + test_url_loader_factory->GetSafeWeakWrapper()); + return signin_client; +} diff --git a/chromium/chrome/browser/signin/chrome_signin_client_test_util.h b/chromium/chrome/browser/signin/chrome_signin_client_test_util.h new file mode 100644 index 00000000000..5c76977980f --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_test_util.h @@ -0,0 +1,26 @@ +// Copyright 2016 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. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_ + +#include <memory> + +class KeyedService; + +namespace content { +class BrowserContext; +} + +namespace network { +class TestURLLoaderFactory; +} + +// Creates a ChromeSigninClient using the supplied +// |test_url_loader_factory| and |context|. +std::unique_ptr<KeyedService> BuildChromeSigninClientWithURLLoader( + network::TestURLLoaderFactory* test_url_loader_factory, + content::BrowserContext* context); + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_CLIENT_TEST_UTIL_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc new file mode 100644 index 00000000000..ed2ac0db988 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_client_unittest.cc @@ -0,0 +1,438 @@ +// 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. + +#include "chrome/browser/signin/chrome_signin_client.h" +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/cxx17_backports.h" +#include "base/feature_list.h" +#include "base/memory/raw_ptr.h" +#include "base/notreached.h" +#include "base/run_loop.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/enterprise/util/managed_browser_utils.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "content/public/browser/network_service_instance.h" +#include "content/public/test/browser_task_environment.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if !defined(OS_ANDROID) +#include "chrome/test/base/browser_with_test_window_test.h" +#endif + +// ChromeOS has its own network delay logic. +#if !BUILDFLAG(IS_CHROMEOS_ASH) + +namespace { + +class CallbackTester { + public: + CallbackTester() : called_(0) {} + + void Increment(); + void IncrementAndUnblock(base::RunLoop* run_loop); + bool WasCalledExactlyOnce(); + + private: + int called_; +}; + +void CallbackTester::Increment() { + called_++; +} + +void CallbackTester::IncrementAndUnblock(base::RunLoop* run_loop) { + Increment(); + run_loop->QuitWhenIdle(); +} + +bool CallbackTester::WasCalledExactlyOnce() { + return called_ == 1; +} + +} // namespace + +class ChromeSigninClientTest : public testing::Test { + public: + ChromeSigninClientTest() { + // Create a signed-in profile. + TestingProfile::Builder builder; + profile_ = builder.Build(); + + signin_client_ = ChromeSigninClientFactory::GetForProfile(profile()); + } + + protected: + void SetUpNetworkConnection(bool respond_synchronously, + network::mojom::ConnectionType connection_type) { + auto* tracker = network::TestNetworkConnectionTracker::GetInstance(); + tracker->SetRespondSynchronously(respond_synchronously); + tracker->SetConnectionType(connection_type); + } + + void SetConnectionType(network::mojom::ConnectionType connection_type) { + network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType( + connection_type); + } + + Profile* profile() { return profile_.get(); } + SigninClient* signin_client() { return signin_client_; } + + private: + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr<Profile> profile_; + raw_ptr<SigninClient> signin_client_; +}; + +TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsImmediatelyWithNetwork) { + SetUpNetworkConnection(true, network::mojom::ConnectionType::CONNECTION_3G); + CallbackTester tester; + signin_client()->DelayNetworkCall( + base::BindOnce(&CallbackTester::Increment, base::Unretained(&tester))); + ASSERT_TRUE(tester.WasCalledExactlyOnce()); +} + +TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsAfterGetConnectionType) { + SetUpNetworkConnection(false, network::mojom::ConnectionType::CONNECTION_3G); + + base::RunLoop run_loop; + CallbackTester tester; + signin_client()->DelayNetworkCall( + base::BindOnce(&CallbackTester::IncrementAndUnblock, + base::Unretained(&tester), &run_loop)); + ASSERT_FALSE(tester.WasCalledExactlyOnce()); + run_loop.Run(); // Wait for IncrementAndUnblock(). + ASSERT_TRUE(tester.WasCalledExactlyOnce()); +} + +TEST_F(ChromeSigninClientTest, DelayNetworkCallRunsAfterNetworkChange) { + SetUpNetworkConnection(true, network::mojom::ConnectionType::CONNECTION_NONE); + + base::RunLoop run_loop; + CallbackTester tester; + signin_client()->DelayNetworkCall( + base::BindOnce(&CallbackTester::IncrementAndUnblock, + base::Unretained(&tester), &run_loop)); + + ASSERT_FALSE(tester.WasCalledExactlyOnce()); + SetConnectionType(network::mojom::ConnectionType::CONNECTION_3G); + run_loop.Run(); // Wait for IncrementAndUnblock(). + ASSERT_TRUE(tester.WasCalledExactlyOnce()); +} + +#if !defined(OS_ANDROID) + +class MockChromeSigninClient : public ChromeSigninClient { + public: + explicit MockChromeSigninClient(Profile* profile) + : ChromeSigninClient(profile) {} + + MOCK_METHOD1(ShowUserManager, void(const base::FilePath&)); + MOCK_METHOD1(LockForceSigninProfile, void(const base::FilePath&)); + + MOCK_METHOD3(SignOutCallback, + void(signin_metrics::ProfileSignout, + signin_metrics::SignoutDelete, + SigninClient::SignoutDecision signout_decision)); +}; + +class ChromeSigninClientSignoutTest : public BrowserWithTestWindowTest { + public: + ChromeSigninClientSignoutTest() : forced_signin_setter_(true) {} + void SetUp() override { + BrowserWithTestWindowTest::SetUp(); + CreateClient(browser()->profile()); + } + + void TearDown() override { + BrowserWithTestWindowTest::TearDown(); + TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr); + } + + void CreateClient(Profile* profile) { + client_ = std::make_unique<MockChromeSigninClient>(profile); + } + + void PreSignOut(signin_metrics::ProfileSignout source_metric, + signin_metrics::SignoutDelete delete_metric) { + client_->PreSignOut(base::BindOnce(&MockChromeSigninClient::SignOutCallback, + base::Unretained(client_.get()), + source_metric, delete_metric), + source_metric); + } + + signin_util::ScopedForceSigninSetterForTesting forced_signin_setter_; + std::unique_ptr<MockChromeSigninClient> client_; +}; + +TEST_F(ChromeSigninClientSignoutTest, SignOut) { + signin_metrics::ProfileSignout source_metric = + signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + + EXPECT_CALL(*client_, ShowUserManager(browser()->profile()->GetPath())) + .Times(1); + EXPECT_CALL(*client_, LockForceSigninProfile(browser()->profile()->GetPath())) + .Times(1); + EXPECT_CALL( + *client_, + SignOutCallback(source_metric, delete_metric, + SigninClient::SignoutDecision::ALLOW_SIGNOUT)) + .Times(1); + + PreSignOut(source_metric, delete_metric); +} + +TEST_F(ChromeSigninClientSignoutTest, SignOutWithoutForceSignin) { + signin_util::ScopedForceSigninSetterForTesting signin_setter(false); + CreateClient(browser()->profile()); + + signin_metrics::ProfileSignout source_metric = + signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + + EXPECT_CALL(*client_, ShowUserManager(browser()->profile()->GetPath())) + .Times(0); + EXPECT_CALL(*client_, LockForceSigninProfile(browser()->profile()->GetPath())) + .Times(0); + EXPECT_CALL( + *client_, + SignOutCallback(source_metric, delete_metric, + SigninClient::SignoutDecision::ALLOW_SIGNOUT)) + .Times(1); + PreSignOut(source_metric, delete_metric); +} + +class ChromeSigninClientSignoutSourceTest + : public ::testing::WithParamInterface<signin_metrics::ProfileSignout>, + public ChromeSigninClientSignoutTest { + protected: + signin::IdentityTestEnvironment* identity_test_env() { + return &identity_test_env_; + } + + private: + signin::IdentityTestEnvironment identity_test_env_; +}; + +// Returns true if signout can be disallowed by policy for the given source. +bool IsSignoutDisallowedByPolicy( + Profile* profile, + signin_metrics::ProfileSignout signout_source) { + auto* identity_manager = + IdentityManagerFactory::GetForProfileIfExists(profile); + if (identity_manager && + !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + return false; + } + + switch (signout_source) { + // NOTE: SIGNOUT_TEST == SIGNOUT_PREF_CHANGED. + case signin_metrics::ProfileSignout::SIGNOUT_PREF_CHANGED: + case signin_metrics::ProfileSignout::GOOGLE_SERVICE_NAME_PATTERN_CHANGED: + case signin_metrics::ProfileSignout::SIGNIN_PREF_CHANGED_DURING_SIGNIN: + case signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS: + case signin_metrics::ProfileSignout::SERVER_FORCED_DISABLE: + case signin_metrics::ProfileSignout::TRANSFER_CREDENTIALS: + case signin_metrics::ProfileSignout:: + AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN: + case signin_metrics::ProfileSignout::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT: + case signin_metrics::ProfileSignout::USER_TUNED_OFF_SYNC_FROM_DICE_UI: + return true; + case signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE: + case signin_metrics::ProfileSignout:: + IOS_ACCOUNT_REMOVED_FROM_DEVICE_AFTER_RESTORE: + // TODO(msarda): Add more of the above cases to this "false" branch. + // For now only ACCOUNT_REMOVED_FROM_DEVICE is here to preserve the status + // quo. Additional internal sources of sign-out will be moved here in a + // follow up CL. + return false; + case signin_metrics::ProfileSignout::ABORT_SIGNIN: + // Allow signout because data has not been synced yet. + return false; + case signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST: + // Allow signout for tests that want to force it. + return false; + case signin_metrics::ProfileSignout::ACCOUNT_ID_MIGRATION: + // Allowed to force finish the account id migration. + return false; + case signin_metrics::ProfileSignout::USER_DELETED_ACCOUNT_COOKIES: + case signin_metrics::ProfileSignout::MOBILE_IDENTITY_CONSISTENCY_ROLLBACK: + // There's no special-casing for these in ChromeSigninClient, as they only + // happen when there's no sync account and policies aren't enforced. + // PrimaryAccountManager won't actually invoke PreSignOut in this case, + // thus it is fine for ChromeSigninClient to not have any special-casing. + return true; + case signin_metrics::ProfileSignout::NUM_PROFILE_SIGNOUT_METRICS: + NOTREACHED(); + return false; + } +} + +TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutAllowed) { + signin_metrics::ProfileSignout signout_source = GetParam(); + + TestingProfile::Builder builder; + builder.SetGuestSession(); + std::unique_ptr<TestingProfile> profile = builder.Build(); + + CreateClient(profile.get()); + ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + + // Verify IdentityManager gets callback indicating sign-out is always allowed. + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + EXPECT_CALL( + *client_, + SignOutCallback(signout_source, delete_metric, + SigninClient::SignoutDecision::ALLOW_SIGNOUT)) + .Times(1); + + PreSignOut(signout_source, delete_metric); +} + +#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \ + defined(OS_MAC) +TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutDisallowed) { + signin_metrics::ProfileSignout signout_source = GetParam(); + + TestingProfile::Builder builder; + builder.SetGuestSession(); + std::unique_ptr<TestingProfile> profile = builder.Build(); + + CreateClient(profile.get()); + + ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + signin_util::SetUserSignoutAllowedForProfile(profile.get(), false); + ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + + // Verify IdentityManager gets callback indicating sign-out is disallowed iff + // the source of the sign-out is a user-action. + SigninClient::SignoutDecision signout_decision = + IsSignoutDisallowedByPolicy(profile.get(), signout_source) + ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT + : SigninClient::SignoutDecision::ALLOW_SIGNOUT; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + EXPECT_CALL(*client_, + SignOutCallback(signout_source, delete_metric, signout_decision)) + .Times(1); + + PreSignOut(signout_source, delete_metric); +} + +TEST_P(ChromeSigninClientSignoutSourceTest, UserSignoutDisallowedWithSync) { + signin_metrics::ProfileSignout signout_source = GetParam(); + + TestingProfile::Builder builder; + builder.SetGuestSession(); + std::unique_ptr<TestingProfile> profile = builder.Build(); + + CreateClient(profile.get()); + + ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + signin_util::SetUserSignoutAllowedForProfile(profile.get(), false); + ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + + // Verify IdentityManager gets callback indicating sign-out is disallowed iff + // the source of the sign-out is a user-action. + SigninClient::SignoutDecision signout_decision = + IsSignoutDisallowedByPolicy(profile.get(), signout_source) + ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT + : SigninClient::SignoutDecision::ALLOW_SIGNOUT; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + identity_test_env()->MakePrimaryAccountAvailable("bob@example.com", + signin::ConsentLevel::kSync); + EXPECT_CALL(*client_, + SignOutCallback(signout_source, delete_metric, signout_decision)) + .Times(1); + + PreSignOut(signout_source, delete_metric); +} + +TEST_P(ChromeSigninClientSignoutSourceTest, + UserSignoutDisallowedAccountManagementAccepted) { + base::test::ScopedFeatureList features(kAccountPoliciesLoadedWithoutSync); + signin_metrics::ProfileSignout signout_source = GetParam(); + + TestingProfile::Builder builder; + builder.SetGuestSession(); + std::unique_ptr<TestingProfile> profile = builder.Build(); + + CreateClient(profile.get()); + + ASSERT_TRUE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + signin_util::SetUserSignoutAllowedForProfile(profile.get(), false); + ASSERT_FALSE(signin_util::IsUserSignoutAllowedForProfile(profile.get())); + + // Verify IdentityManager gets callback indicating sign-out is disallowed iff + // the source of the sign-out is a user-action. + SigninClient::SignoutDecision signout_decision = + IsSignoutDisallowedByPolicy(profile.get(), signout_source) + ? SigninClient::SignoutDecision::DISALLOW_SIGNOUT + : SigninClient::SignoutDecision::ALLOW_SIGNOUT; + signin_metrics::SignoutDelete delete_metric = + signin_metrics::SignoutDelete::kIgnoreMetric; + EXPECT_CALL(*client_, + SignOutCallback(signout_source, delete_metric, signout_decision)) + .Times(1); + + PreSignOut(signout_source, delete_metric); +} +#endif + +const signin_metrics::ProfileSignout kSignoutSources[] = { + signin_metrics::ProfileSignout::SIGNOUT_PREF_CHANGED, + signin_metrics::ProfileSignout::GOOGLE_SERVICE_NAME_PATTERN_CHANGED, + signin_metrics::ProfileSignout::SIGNIN_PREF_CHANGED_DURING_SIGNIN, + signin_metrics::ProfileSignout::USER_CLICKED_SIGNOUT_SETTINGS, + signin_metrics::ProfileSignout::ABORT_SIGNIN, + signin_metrics::ProfileSignout::SERVER_FORCED_DISABLE, + signin_metrics::ProfileSignout::TRANSFER_CREDENTIALS, + signin_metrics::ProfileSignout::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN, + signin_metrics::ProfileSignout::USER_TUNED_OFF_SYNC_FROM_DICE_UI, + signin_metrics::ProfileSignout::ACCOUNT_REMOVED_FROM_DEVICE, + signin_metrics::ProfileSignout::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT, + signin_metrics::ProfileSignout::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST, + signin_metrics::ProfileSignout::USER_DELETED_ACCOUNT_COOKIES, + signin_metrics::ProfileSignout::MOBILE_IDENTITY_CONSISTENCY_ROLLBACK, + signin_metrics::ProfileSignout::ACCOUNT_ID_MIGRATION, + signin_metrics::ProfileSignout:: + IOS_ACCOUNT_REMOVED_FROM_DEVICE_AFTER_RESTORE, +}; +static_assert(base::size(kSignoutSources) == + signin_metrics::ProfileSignout::NUM_PROFILE_SIGNOUT_METRICS, + "kSignoutSources should enumerate all ProfileSignout values"); + +INSTANTIATE_TEST_SUITE_P(AllSignoutSources, + ChromeSigninClientSignoutSourceTest, + testing::ValuesIn(kSignoutSources)); + +#endif // !defined(OS_ANDROID) +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) diff --git a/chromium/chrome/browser/signin/chrome_signin_helper.cc b/chromium/chrome/browser/signin/chrome_signin_helper.cc new file mode 100644 index 00000000000..b85dde43783 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_helper.cc @@ -0,0 +1,712 @@ +// Copyright 2013 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. + +#include "chrome/browser/signin/chrome_signin_helper.h" + +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/metrics/histogram_functions.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/supports_user_data.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/prefs/incognito_mode_prefs.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_io_data.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/chrome_signin_client.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/cookie_reminter_factory.h" +#include "chrome/browser/signin/dice_response_handler.h" +#include "chrome/browser/signin/header_modification_delegate_impl.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/process_dice_header_delegate_impl.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/tab_contents/tab_util.h" +#include "chrome/browser/ui/profile_picker.h" +#include "chrome/browser/ui/webui/signin/login_ui_service.h" +#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h" +#include "chrome/browser/ui/webui/signin/signin_ui_error.h" +#include "chrome/common/url_constants.h" +#include "components/account_manager_core/account_manager_facade.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/cookie_reminter.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/identity_manager/accounts_cookie_mutator.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "net/http/http_response_headers.h" + +#if defined(OS_ANDROID) +#include "chrome/browser/android/signin/signin_bridge.h" +#include "ui/android/view_android.h" +#else +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window.h" +#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h" +#endif // defined(OS_ANDROID) + +#if defined(OS_CHROMEOS) +#include "chrome/browser/profiles/profile_manager.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif // defined(OS_CHROMEOS) + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/supervised_user/supervised_user_service.h" +#include "chrome/browser/supervised_user/supervised_user_service_factory.h" +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "chrome/browser/lacros/account_manager/account_manager_util.h" +#include "chrome/browser/lacros/account_manager/account_profile_mapper.h" +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#endif + +namespace signin { + +const void* const kManageAccountsHeaderReceivedUserDataKey = + &kManageAccountsHeaderReceivedUserDataKey; + +const char kChromeMirrorHeaderSource[] = "Chrome"; + +namespace { + +// Key for RequestDestructionObserverUserData. +const void* const kRequestDestructionObserverUserDataKey = + &kRequestDestructionObserverUserDataKey; + +const char kGoogleRemoveLocalAccountResponseHeader[] = + "Google-Accounts-RemoveLocalAccount"; + +const char kRemoveLocalAccountObfuscatedIDAttrName[] = "obfuscatedid"; + +// TODO(droger): Remove this delay when the Dice implementation is finished on +// the server side. +int g_dice_account_reconcilor_blocked_delay_ms = 1000; + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + +const char kGoogleSignoutResponseHeader[] = "Google-Accounts-SignOut"; + +// Refcounted wrapper that facilitates creating and deleting a +// AccountReconcilor::Lock. +class AccountReconcilorLockWrapper + : public base::RefCountedThreadSafe<AccountReconcilorLockWrapper> { + public: + explicit AccountReconcilorLockWrapper( + const content::WebContents::Getter& web_contents_getter) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + content::WebContents* web_contents = web_contents_getter.Run(); + if (!web_contents) + return; + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + AccountReconcilor* account_reconcilor = + AccountReconcilorFactory::GetForProfile(profile); + account_reconcilor_lock_ = + std::make_unique<AccountReconcilor::Lock>(account_reconcilor); + } + + AccountReconcilorLockWrapper(const AccountReconcilorLockWrapper&) = delete; + AccountReconcilorLockWrapper& operator=(const AccountReconcilorLockWrapper&) = + delete; + + void DestroyAfterDelay() { + // TODO(dcheng): Should ReleaseSoon() support this use case? + content::GetUIThreadTaskRunner({})->PostDelayedTask( + FROM_HERE, + base::BindOnce([](scoped_refptr<AccountReconcilorLockWrapper>) {}, + base::RetainedRef(this)), + base::Milliseconds(g_dice_account_reconcilor_blocked_delay_ms)); + } + + private: + friend class base::RefCountedThreadSafe<AccountReconcilorLockWrapper>; + ~AccountReconcilorLockWrapper() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + } + + std::unique_ptr<AccountReconcilor::Lock> account_reconcilor_lock_; +}; + +// Returns true if the account reconcilor needs be be blocked while a Gaia +// sign-in request is in progress. +// +// The account reconcilor must be blocked on all request that may change the +// Gaia authentication cookies. This includes: +// * Main frame requests. +// * XHR requests having Gaia URL as referrer. +bool ShouldBlockReconcilorForRequest(ChromeRequestAdapter* request) { + if (request->GetRequestDestination() == + network::mojom::RequestDestination::kDocument) { + return true; + } + + return request->IsFetchLikeAPI() && + gaia::IsGaiaSignonRealm(request->GetReferrerOrigin()); +} + +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +void OnLacrosAccountsAvailableAsSecondaryFetched( + AccountProfileMapper* mapper, + const base::FilePath& profile_path, + const std::vector<account_manager::Account>& accounts) { + if (!accounts.empty()) { + // Pass in the current profile to signal that the user wants to select a + // _secondary_ account for this particular profile. + ProfilePicker::Show( + ProfilePicker::EntryPoint::kLacrosSelectAvailableAccount, GURL(), + profile_path); + return; + } + mapper->ShowAddAccountDialog(profile_path, + account_manager::AccountManagerFacade:: + AccountAdditionSource::kOgbAddAccount, + AccountProfileMapper::AddAccountCallback()); +} +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +class RequestDestructionObserverUserData : public base::SupportsUserData::Data { + public: + explicit RequestDestructionObserverUserData(base::OnceClosure closure) + : closure_(std::move(closure)) {} + + RequestDestructionObserverUserData( + const RequestDestructionObserverUserData&) = delete; + RequestDestructionObserverUserData& operator=( + const RequestDestructionObserverUserData&) = delete; + + ~RequestDestructionObserverUserData() override { std::move(closure_).Run(); } + + private: + base::OnceClosure closure_; +}; + +// This user data is used as a marker that a Mirror header was found on the +// redirect chain. It does not contain any data, its presence is enough to +// indicate that a header has already be found on the request. +class ManageAccountsHeaderReceivedUserData + : public base::SupportsUserData::Data {}; + +#if BUILDFLAG(ENABLE_MIRROR) +// Processes the mirror response header on the UI thread. Currently depending +// on the value of |header_value|, it either shows the profile avatar menu, or +// opens an incognito window/tab. +void ProcessMirrorHeader( + ManageAccountsParams manage_accounts_params, + const content::WebContents::Getter& web_contents_getter) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + GAIAServiceType service_type = manage_accounts_params.service_type; + DCHECK_NE(GAIA_SERVICE_TYPE_NONE, service_type); + + content::WebContents* web_contents = web_contents_getter.Run(); + if (!web_contents) + return; + + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + DCHECK(AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile)) + << "Gaia should not send the X-Chrome-Manage-Accounts header " + << "when Mirror is disabled."; + AccountReconcilor* account_reconcilor = + AccountReconcilorFactory::GetForProfile(profile); + account_reconcilor->OnReceivedManageAccountsResponse(service_type); + +#if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS) + signin_metrics::LogAccountReconcilorStateOnGaiaResponse( + account_reconcilor->GetState()); + + bool should_ignore_guest_webview = true; +#if BUILDFLAG(ENABLE_EXTENSIONS) + // The mirror headers from some guest web views need to be processed. + should_ignore_guest_webview = + HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest( + web_contents); +#endif + + Browser* browser = chrome::FindBrowserWithWebContents(web_contents); + // Do not do anything if the navigation happened in the "background". + if ((!browser || !browser->window()->IsActive()) && + should_ignore_guest_webview) { + return; + } + + // Record the service type. + base::UmaHistogramEnumeration("AccountManager.ManageAccountsServiceType", + service_type); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + // Ignore response to background request from another profile, so dialogs are + // not displayed in the wrong profile when using ChromeOS multiprofile mode. + if (profile != ProfileManager::GetActiveUserProfile()) + return; +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + + // The only allowed operations are: + // 1. Going Incognito. + // 2. Displaying a reauthentication window: Enterprise GSuite Accounts could + // have been forced through an online in-browser sign-in for sensitive + // webpages, thereby decreasing their session validity. After their session + // expires, they will receive a "Mirror" re-authentication request for all + // Google web properties. Another case when this can be triggered is + // https://crbug.com/1012649. + // 3. Displaying an account addition window: when user clicks "Add another + // account" in One Google Bar. + // 4. Displaying the Account Manager for managing accounts. + + // 1. Going incognito. + if (service_type == GAIA_SERVICE_TYPE_INCOGNITO) { + chrome::NewIncognitoWindow(profile); + return; + } + + // 2. Displaying a reauthentication window + if (!manage_accounts_params.email.empty()) { + // TODO(https://crbug.com/1226055): enable this for lacros. +#if BUILDFLAG(IS_CHROMEOS_ASH) + // Do not display the re-authentication dialog if this event was triggered + // by supervision being enabled for an account. In this situation, a + // complete signout is required. + SupervisedUserService* service = + SupervisedUserServiceFactory::GetForProfile(profile); + if (service && service->signout_required_after_supervision_enabled()) { + return; + } +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + // Child users shouldn't get the re-authentication dialog for primary + // account. Log out all accounts to re-mint the cookies. + // (See the reason below.) + signin::IdentityManager* const identity_manager = + IdentityManagerFactory::GetForProfile(profile); + CoreAccountInfo primary_account = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + if (profile->IsChild() && + gaia::AreEmailsSame(primary_account.email, + manage_accounts_params.email)) { + identity_manager->GetAccountsCookieMutator()->LogOutAllAccounts( + gaia::GaiaSource::kChromeOS, base::DoNothing()); + return; + } + + // The account's cookie is invalid but the cookie has not been removed by + // |AccountReconcilor|. Ideally, this should not happen. At this point, + // |AccountReconcilor| cannot detect this state because its source of truth + // (/ListAccounts) is giving us false positives (claiming an invalid account + // to be valid). We need to store that this account's cookie is actually + // invalid, so that if/when this account is re-authenticated, we can force a + // reconciliation for this account instead of treating it as a no-op. + // See https://crbug.com/1012649 for details. + AccountInfo maybe_account_info = + identity_manager->FindExtendedAccountInfoByEmailAddress( + manage_accounts_params.email); + if (!maybe_account_info.IsEmpty()) { + CookieReminter* const cookie_reminter = + CookieReminterFactory::GetForProfile(profile); + cookie_reminter->ForceCookieRemintingOnNextTokenUpdate( + maybe_account_info); + } + + // Display a re-authentication dialog. + ::GetAccountManagerFacade(profile->GetPath().value()) + ->ShowReauthAccountDialog(account_manager::AccountManagerFacade:: + AccountAdditionSource::kContentAreaReauth, + manage_accounts_params.email); + return; + } + + // 3. Displaying an account addition window. + if (service_type == GAIA_SERVICE_TYPE_ADDSESSION) { +#if BUILDFLAG(IS_CHROMEOS_LACROS) + AccountProfileMapper* mapper = + g_browser_process->profile_manager()->GetAccountProfileMapper(); + GetAccountsAvailableAsSecondary( + mapper, profile->GetPath(), + // It's safe to bind raw `mapper`, the callback gets called iff + // `mapper` is still valid. + base::BindOnce(&OnLacrosAccountsAvailableAsSecondaryFetched, mapper, + profile->GetPath())); +#else + ::GetAccountManagerFacade(profile->GetPath().value()) + ->ShowAddAccountDialog(account_manager::AccountManagerFacade:: + AccountAdditionSource::kOgbAddAccount); +#endif + return; + } + + // 4. Displaying the Account Manager for managing accounts. + ::GetAccountManagerFacade(profile->GetPath().value()) + ->ShowManageAccountsSettings(); + return; + +#elif defined(OS_ANDROID) + if (manage_accounts_params.show_consistency_promo) { + auto* window = web_contents->GetNativeView()->GetWindowAndroid(); + if (!window) { + // The page is prefetched in the background, ignore the header. + // See https://crbug.com/1145031#c5 for details. + return; + } + SigninBridge::OpenAccountPickerBottomSheet( + window, manage_accounts_params.continue_url.empty() + ? chrome::kChromeUINativeNewTabURL + : manage_accounts_params.continue_url); + return; + } + if (service_type == signin::GAIA_SERVICE_TYPE_INCOGNITO) { + GURL url(manage_accounts_params.continue_url.empty() + ? chrome::kChromeUINativeNewTabURL + : manage_accounts_params.continue_url); + web_contents->OpenURL(content::OpenURLParams( + url, content::Referrer(), WindowOpenDisposition::OFF_THE_RECORD, + ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false)); + } else { + signin_metrics::LogAccountReconcilorStateOnGaiaResponse( + account_reconcilor->GetState()); + auto* window = web_contents->GetNativeView()->GetWindowAndroid(); + if (!window) + return; + SigninBridge::OpenAccountManagementScreen(window, service_type); + } +#endif // BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS) +} +#endif // BUILDFLAG(ENABLE_MIRROR) + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + +// Creates a DiceTurnOnSyncHelper. +void CreateDiceTurnOnSyncHelper(Profile* profile, + signin_metrics::AccessPoint access_point, + signin_metrics::PromoAction promo_action, + signin_metrics::Reason reason, + content::WebContents* web_contents, + const CoreAccountId& account_id) { + DCHECK(profile); + Browser* browser = web_contents + ? chrome::FindBrowserWithWebContents(web_contents) + : chrome::FindBrowserWithProfile(profile); + // DiceTurnSyncOnHelper is suicidal (it will kill itself once it finishes + // enabling sync). + new DiceTurnSyncOnHelper( + profile, browser, access_point, promo_action, reason, account_id, + DiceTurnSyncOnHelper::SigninAbortedMode::REMOVE_ACCOUNT); +} + +// Shows UI for signin errors. +void ShowDiceSigninError(Profile* profile, + content::WebContents* web_contents, + const SigninUIError& error) { + DCHECK(profile); + Browser* browser = web_contents + ? chrome::FindBrowserWithWebContents(web_contents) + : chrome::FindBrowserWithProfile(profile); + LoginUIServiceFactory::GetForProfile(profile)->DisplayLoginResult(browser, + error); +} + +void ProcessDiceHeader( + const DiceResponseParams& dice_params, + const content::WebContents::Getter& web_contents_getter) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + content::WebContents* web_contents = web_contents_getter.Run(); + if (!web_contents) + return; + + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + DCHECK(!profile->IsOffTheRecord()); + + // Ignore Dice response headers if Dice is not enabled. + if (!AccountConsistencyModeManager::IsDiceEnabledForProfile(profile)) + return; + + DiceResponseHandler* dice_response_handler = + DiceResponseHandler::GetForProfile(profile); + dice_response_handler->ProcessDiceHeader( + dice_params, + std::make_unique<ProcessDiceHeaderDelegateImpl>( + web_contents, base::BindOnce(&CreateDiceTurnOnSyncHelper), + base::BindOnce(&ShowDiceSigninError))); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(ENABLE_MIRROR) +// Looks for the X-Chrome-Manage-Accounts response header, and if found, +// tries to show the avatar bubble in the browser identified by the +// child/route id. Must be called on IO thread. +void ProcessMirrorResponseHeaderIfExists(ResponseAdapter* response, + bool is_off_the_record) { + CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin())); + + if (!response->IsMainFrame()) + return; + + const net::HttpResponseHeaders* response_headers = response->GetHeaders(); + if (!response_headers) + return; + + std::string header_value; + if (!response_headers->GetNormalizedHeader(kChromeManageAccountsHeader, + &header_value)) { + return; + } + + if (is_off_the_record) { + NOTREACHED() << "Gaia should not send the X-Chrome-Manage-Accounts header " + << "in incognito."; + return; + } + + ManageAccountsParams params = BuildManageAccountsParams(header_value); + // If the request does not have a response header or if the header contains + // garbage, then |service_type| is set to |GAIA_SERVICE_TYPE_NONE|. + if (params.service_type == GAIA_SERVICE_TYPE_NONE) + return; + + // Only process one mirror header per request (multiple headers on the same + // redirect chain are ignored). + if (response->GetUserData(kManageAccountsHeaderReceivedUserDataKey)) { + LOG(ERROR) << "Multiple X-Chrome-Manage-Accounts headers on a redirect " + << "chain, ignoring"; + return; + } + + response->SetUserData( + kManageAccountsHeaderReceivedUserDataKey, + std::make_unique<ManageAccountsHeaderReceivedUserData>()); + + // Post a task even if we are already on the UI thread to avoid making any + // requests while processing a throttle event. + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(ProcessMirrorHeader, params, + response->GetWebContentsGetter())); +} +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +void ProcessDiceResponseHeaderIfExists(ResponseAdapter* response, + bool is_off_the_record) { + CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin())); + + if (is_off_the_record) + return; + + const net::HttpResponseHeaders* response_headers = response->GetHeaders(); + if (!response_headers) + return; + + std::string header_value; + DiceResponseParams params; + if (response_headers->GetNormalizedHeader(kDiceResponseHeader, + &header_value)) { + params = BuildDiceSigninResponseParams(header_value); + // The header must be removed for privacy reasons, so that renderers never + // have access to the authorization code. + response->RemoveHeader(kDiceResponseHeader); + } else if (response_headers->GetNormalizedHeader(kGoogleSignoutResponseHeader, + &header_value)) { + params = BuildDiceSignoutResponseParams(header_value); + } + + // If the request does not have a response header or if the header contains + // garbage, then |user_intention| is set to |NONE|. + if (params.user_intention == DiceAction::NONE) + return; + + // Post a task even if we are already on the UI thread to avoid making any + // requests while processing a throttle event. + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(ProcessDiceHeader, std::move(params), + response->GetWebContentsGetter())); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +std::string ParseGaiaIdFromRemoveLocalAccountResponseHeader( + const net::HttpResponseHeaders* response_headers) { + if (!response_headers) + return std::string(); + + std::string header_value; + if (!response_headers->GetNormalizedHeader( + kGoogleRemoveLocalAccountResponseHeader, &header_value)) { + return std::string(); + } + + const SigninHeaderHelper::ResponseHeaderDictionary header_dictionary = + SigninHeaderHelper::ParseAccountConsistencyResponseHeader(header_value); + + std::string gaia_id; + const auto it = + header_dictionary.find(kRemoveLocalAccountObfuscatedIDAttrName); + if (it != header_dictionary.end()) { + // The Gaia ID is wrapped in quotes. + base::TrimString(it->second, "\"", &gaia_id); + } + return gaia_id; +} + +void ProcessRemoveLocalAccountResponseHeaderIfExists(ResponseAdapter* response, + bool is_off_the_record) { + CHECK(gaia::IsGaiaSignonRealm(response->GetOrigin())); + + if (is_off_the_record) + return; + + const std::string gaia_id = + ParseGaiaIdFromRemoveLocalAccountResponseHeader(response->GetHeaders()); + + if (gaia_id.empty()) + return; + + content::WebContents* web_contents = response->GetWebContentsGetter().Run(); + // The tab could have just closed. Technically, it would be possible to + // refactor the code to pass around the profile by other means, but this + // should be rare enough to be worth supporting. + if (!web_contents) + return; + + Profile* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + DCHECK(!profile->IsOffTheRecord()); + + IdentityManagerFactory::GetForProfile(profile) + ->GetAccountsCookieMutator() + ->RemoveLoggedOutAccountByGaiaId(gaia_id); +} + +} // namespace + +ChromeRequestAdapter::ChromeRequestAdapter( + const GURL& url, + const net::HttpRequestHeaders& original_headers, + net::HttpRequestHeaders* modified_headers, + std::vector<std::string>* headers_to_remove) + : RequestAdapter(url, + original_headers, + modified_headers, + headers_to_remove) {} + +ChromeRequestAdapter::~ChromeRequestAdapter() = default; + +ResponseAdapter::ResponseAdapter() = default; + +ResponseAdapter::~ResponseAdapter() = default; + +void SetDiceAccountReconcilorBlockDelayForTesting(int delay_ms) { + g_dice_account_reconcilor_blocked_delay_ms = delay_ms; +} + +void FixAccountConsistencyRequestHeader( + ChromeRequestAdapter* request, + const GURL& redirect_url, + bool is_off_the_record, + int incognito_availibility, + AccountConsistencyMethod account_consistency, + const std::string& gaia_id, + signin::Tribool is_child_account, +#if BUILDFLAG(IS_CHROMEOS_ASH) + bool is_secondary_account_addition_allowed, +#endif +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + bool is_sync_enabled, + const std::string& signin_scoped_device_id, +#endif + content_settings::CookieSettings* cookie_settings) { + if (is_off_the_record) + return; // Account consistency is disabled in incognito. + + // If new url is eligible to have the header, add it, otherwise remove it. + +// Mirror header: +#if BUILDFLAG(ENABLE_MIRROR) + int profile_mode_mask = PROFILE_MODE_DEFAULT; + if (incognito_availibility == + static_cast<int>(IncognitoModePrefs::Availability::kDisabled) || + IncognitoModePrefs::ArePlatformParentalControlsEnabled()) { + profile_mode_mask |= PROFILE_MODE_INCOGNITO_DISABLED; + } + +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (!is_secondary_account_addition_allowed) { + account_consistency = AccountConsistencyMethod::kMirror; + // Can't add new accounts. + profile_mode_mask |= PROFILE_MODE_ADD_ACCOUNT_DISABLED; + } +#endif + + AppendOrRemoveMirrorRequestHeader( + request, redirect_url, gaia_id, is_child_account, account_consistency, + cookie_settings, profile_mode_mask, kChromeMirrorHeaderSource, + /*force_account_consistency=*/false); +#endif // BUILDFLAG(ENABLE_MIRROR) + +// Dice header: +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + bool dice_header_added = AppendOrRemoveDiceRequestHeader( + request, redirect_url, gaia_id, is_sync_enabled, account_consistency, + cookie_settings, signin_scoped_device_id); + + // Block the AccountReconcilor while the Dice requests are in flight. This + // allows the DiceReponseHandler to process the response before the reconcilor + // starts. + if (dice_header_added && ShouldBlockReconcilorForRequest(request)) { + auto lock_wrapper = base::MakeRefCounted<AccountReconcilorLockWrapper>( + request->GetWebContentsGetter()); + // On destruction of the request |lock_wrapper| will be released. + request->SetDestructionCallback(base::BindOnce( + &AccountReconcilorLockWrapper::DestroyAfterDelay, lock_wrapper)); + } +#endif +} + +void ProcessAccountConsistencyResponseHeaders(ResponseAdapter* response, + const GURL& redirect_url, + bool is_off_the_record) { + if (!gaia::IsGaiaSignonRealm(response->GetOrigin())) + return; + +#if BUILDFLAG(ENABLE_MIRROR) + // See if the response contains the X-Chrome-Manage-Accounts header. If so + // show the profile avatar bubble so that user can complete signin/out + // action the native UI. + ProcessMirrorResponseHeaderIfExists(response, is_off_the_record); +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + // Process the Dice header: on sign-in, exchange the authorization code for a + // refresh token, on sign-out just follow the sign-out URL. + ProcessDiceResponseHeaderIfExists(response, is_off_the_record); +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + + if (base::FeatureList::IsEnabled(kProcessGaiaRemoveLocalAccountHeader)) { + ProcessRemoveLocalAccountResponseHeaderIfExists(response, + is_off_the_record); + } +} + +std::string ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + const net::HttpResponseHeaders* response_headers) { + return ParseGaiaIdFromRemoveLocalAccountResponseHeader(response_headers); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chrome_signin_helper.h b/chromium/chrome/browser/signin/chrome_signin_helper.h new file mode 100644 index 00000000000..7d6ea870458 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_helper.h @@ -0,0 +1,129 @@ +// Copyright 2013 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. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_ + +#include <memory> +#include <string> + +#include "base/supports_user_data.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/prefs/incognito_mode_prefs.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "content/public/browser/web_contents.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" + +namespace content_settings { +class CookieSettings; +} + +namespace net { +class HttpResponseHeaders; +} + +class GURL; + +// Utility functions for handling Chrome/Gaia headers during signin process. +// Chrome identity should always stay in sync with Gaia identity. Therefore +// Chrome needs to send Gaia special header for requests from a connected +// profile, so that Gaia can modify its response accordingly and let Chrome +// handle signin accordingly. +namespace signin { + +enum class Tribool; + +// Key for ManageAccountsHeaderReceivedUserData. Exposed for testing. +extern const void* const kManageAccountsHeaderReceivedUserDataKey; + +// The source to use when constructing the Mirror header. +extern const char kChromeMirrorHeaderSource[]; + +class ChromeRequestAdapter : public RequestAdapter { + public: + ChromeRequestAdapter(const GURL& url, + const net::HttpRequestHeaders& original_headers, + net::HttpRequestHeaders* modified_headers, + std::vector<std::string>* headers_to_remove); + + ChromeRequestAdapter(const ChromeRequestAdapter&) = delete; + ChromeRequestAdapter& operator=(const ChromeRequestAdapter&) = delete; + + ~ChromeRequestAdapter() override; + + virtual content::WebContents::Getter GetWebContentsGetter() const = 0; + + virtual network::mojom::RequestDestination GetRequestDestination() const = 0; + + virtual bool IsFetchLikeAPI() const = 0; + + virtual GURL GetReferrerOrigin() const = 0; + + // Associate a callback with this request which will be executed when the + // request is complete (including any redirects). If a callback was already + // registered this function does nothing. + virtual void SetDestructionCallback(base::OnceClosure closure) = 0; +}; + +class ResponseAdapter { + public: + ResponseAdapter(); + + ResponseAdapter(const ResponseAdapter&) = delete; + ResponseAdapter& operator=(const ResponseAdapter&) = delete; + + virtual ~ResponseAdapter(); + + virtual content::WebContents::Getter GetWebContentsGetter() const = 0; + virtual bool IsMainFrame() const = 0; + virtual GURL GetOrigin() const = 0; + virtual const net::HttpResponseHeaders* GetHeaders() const = 0; + virtual void RemoveHeader(const std::string& name) = 0; + + virtual base::SupportsUserData::Data* GetUserData(const void* key) const = 0; + virtual void SetUserData( + const void* key, + std::unique_ptr<base::SupportsUserData::Data> data) = 0; +}; + +// When Dice is enabled, the AccountReconcilor is blocked for a short delay +// after sending requests to Gaia. Exposed for testing. +void SetDiceAccountReconcilorBlockDelayForTesting(int delay_ms); + +// Adds an account consistency header to Gaia requests from a connected profile, +// with the exception of requests from gaia webview. +// Removes the header if it is already in the headers but should not be there. +void FixAccountConsistencyRequestHeader( + ChromeRequestAdapter* request, + const GURL& redirect_url, + bool is_off_the_record, + int incognito_availibility, + AccountConsistencyMethod account_consistency, + const std::string& gaia_id, + signin::Tribool is_child_account, +#if BUILDFLAG(IS_CHROMEOS_ASH) + bool is_secondary_account_addition_allowed, +#endif +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + bool is_sync_enabled, + const std::string& signin_scoped_device_id, +#endif + content_settings::CookieSettings* cookie_settings); + +// Processes account consistency response headers (X-Chrome-Manage-Accounts and +// Dice). |redirect_url| is empty if the request is not a redirect. +void ProcessAccountConsistencyResponseHeaders(ResponseAdapter* response, + const GURL& redirect_url, + bool is_off_the_record); + +// Parses and returns an account ID (Gaia ID) from HTTP response header +// Google-Accounts-RemoveLocalAccount. Returns an empty string if parsing +// failed. Exposed for testing purposes. +std::string ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + const net::HttpResponseHeaders* response_headers); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_HELPER_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc new file mode 100644 index 00000000000..8a5757aabe3 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_helper_unittest.cc @@ -0,0 +1,182 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/chrome_signin_helper.h" + +#include <memory> +#include <string> + +#include "base/run_loop.h" +#include "base/strings/stringprintf.h" +#include "build/chromeos_buildflags.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "content/public/test/browser_task_environment.h" +#include "net/http/http_response_headers.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_context.h" +#include "net/url_request/url_request_filter.h" +#include "net/url_request/url_request_interceptor.h" +#include "net/url_request/url_request_test_job.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { + +#if BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS) +const char kChromeManageAccountsHeader[] = "X-Chrome-Manage-Accounts"; +const char kMirrorAction[] = "action=ADDSESSION"; +#endif + +// URLRequestInterceptor adding a account consistency response header to Gaia +// responses. +class TestRequestInterceptor : public net::URLRequestInterceptor { + public: + explicit TestRequestInterceptor(const std::string& header_name, + const std::string& header_value) + : header_name_(header_name), header_value_(header_value) {} + ~TestRequestInterceptor() override = default; + + private: + std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( + net::URLRequest* request) const override { + std::string response_headers = + base::StringPrintf("HTTP/1.1 200 OK\n\n%s: %s\n", header_name_.c_str(), + header_value_.c_str()); + return std::make_unique<net::URLRequestTestJob>(request, response_headers, + "", true); + } + + const std::string header_name_; + const std::string header_value_; +}; + +class TestResponseAdapter : public signin::ResponseAdapter, + public base::SupportsUserData { + public: + TestResponseAdapter(const std::string& header_name, + const std::string& header_value, + bool is_main_frame) + : is_main_frame_(is_main_frame), + headers_(new net::HttpResponseHeaders(std::string())) { + headers_->SetHeader(header_name, header_value); + } + + TestResponseAdapter(const TestResponseAdapter&) = delete; + TestResponseAdapter& operator=(const TestResponseAdapter&) = delete; + + ~TestResponseAdapter() override {} + + content::WebContents::Getter GetWebContentsGetter() const override { + return base::BindRepeating( + []() -> content::WebContents* { return nullptr; }); + } + bool IsMainFrame() const override { return is_main_frame_; } + GURL GetOrigin() const override { + return GURL("https://accounts.google.com"); + } + const net::HttpResponseHeaders* GetHeaders() const override { + return headers_.get(); + } + + void RemoveHeader(const std::string& name) override { + headers_->RemoveHeader(name); + } + + base::SupportsUserData::Data* GetUserData(const void* key) const override { + return base::SupportsUserData::GetUserData(key); + } + + void SetUserData( + const void* key, + std::unique_ptr<base::SupportsUserData::Data> data) override { + return base::SupportsUserData::SetUserData(key, std::move(data)); + } + + private: + bool is_main_frame_; + scoped_refptr<net::HttpResponseHeaders> headers_; +}; + +} // namespace + +class ChromeSigninHelperTest : public testing::Test { + protected: + ChromeSigninHelperTest() + : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {} + + ~ChromeSigninHelperTest() override = default; + + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr<net::TestDelegate> test_request_delegate_; +}; + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +// Tests that Dice response headers are removed after being processed. +TEST_F(ChromeSigninHelperTest, RemoveDiceSigninHeader) { + // Process the header. + TestResponseAdapter adapter(signin::kDiceResponseHeader, "Foo", + /*is_main_frame=*/false); + signin::ProcessAccountConsistencyResponseHeaders(&adapter, GURL(), + false /* is_incognito */); + + // Check that the header has been removed. + EXPECT_FALSE(adapter.GetHeaders()->HasHeader(signin::kDiceResponseHeader)); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS) +// Tests that user data is set on Mirror requests. +TEST_F(ChromeSigninHelperTest, MirrorMainFrame) { + // Process the header. + TestResponseAdapter response_adapter(kChromeManageAccountsHeader, + kMirrorAction, + /*is_main_frame=*/true); + signin::ProcessAccountConsistencyResponseHeaders(&response_adapter, GURL(), + false /* is_incognito */); + // Check that the header has not been removed. + EXPECT_TRUE( + response_adapter.GetHeaders()->HasHeader(kChromeManageAccountsHeader)); + // Request was flagged with the user data. + EXPECT_TRUE(response_adapter.GetUserData( + signin::kManageAccountsHeaderReceivedUserDataKey)); +} + +// Tests that user data is not set on Mirror requests for sub frames. +TEST_F(ChromeSigninHelperTest, MirrorSubFrame) { + // Process the header. + TestResponseAdapter response_adapter(kChromeManageAccountsHeader, + kMirrorAction, + /*is_main_frame=*/false); + signin::ProcessAccountConsistencyResponseHeaders(&response_adapter, GURL(), + false /* is_incognito */); + // Request was not flagged with the user data. + EXPECT_FALSE(response_adapter.GetUserData( + signin::kManageAccountsHeaderReceivedUserDataKey)); +} +#endif // BUILDFLAG(ENABLE_MIRROR) || BUILDFLAG(IS_CHROMEOS_LACROS) + +TEST_F(ChromeSigninHelperTest, + ParseGaiaIdFromRemoveLocalAccountResponseHeader) { + EXPECT_EQ("123456", + signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + TestResponseAdapter("Google-Accounts-RemoveLocalAccount", + "obfuscatedid=\"123456\"", + /*is_main_frame=*/false) + .GetHeaders())); + EXPECT_EQ("123456", + signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + TestResponseAdapter("Google-Accounts-RemoveLocalAccount", + "obfuscatedid=\"123456\",foo=\"bar\"", + /*is_main_frame=*/false) + .GetHeaders())); + EXPECT_EQ( + "", + signin::ParseGaiaIdFromRemoveLocalAccountResponseHeaderForTesting( + TestResponseAdapter("Google-Accounts-RemoveLocalAccount", "malformed", + /*is_main_frame=*/false) + .GetHeaders())); +} diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc new file mode 100644 index 00000000000..d4b8d911a9e --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.cc @@ -0,0 +1,551 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h" + +#include "base/barrier_closure.h" +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/memory/raw_ptr.h" +#include "base/supports_user_data.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "chrome/browser/signin/header_modification_delegate_impl.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h" +#include "extensions/buildflags/buildflags.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "net/base/net_errors.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/early_hints.mojom.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" +#include "services/network/public/mojom/url_loader.mojom.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" +#include "services/network/public/mojom/url_response_head.mojom.h" + +#if defined(OS_ANDROID) +#include "chrome/browser/android/tab_android.h" +#include "chrome/browser/android/tab_web_contents_delegate_android.h" +#endif + +namespace signin { + +namespace { + +// User data key for BrowserContextData. +const void* const kBrowserContextUserDataKey = &kBrowserContextUserDataKey; + +// Owns all of the ProxyingURLLoaderFactorys for a given Profile. +class BrowserContextData : public base::SupportsUserData::Data { + public: + BrowserContextData(const BrowserContextData&) = delete; + BrowserContextData& operator=(const BrowserContextData&) = delete; + + ~BrowserContextData() override {} + + static void StartProxying( + Profile* profile, + content::WebContents::Getter web_contents_getter, + mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver, + mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory) { + auto* self = static_cast<BrowserContextData*>( + profile->GetUserData(kBrowserContextUserDataKey)); + if (!self) { + self = new BrowserContextData(); + profile->SetUserData(kBrowserContextUserDataKey, base::WrapUnique(self)); + } + +#if defined(OS_ANDROID) + bool is_custom_tab = false; + content::WebContents* web_contents = web_contents_getter.Run(); + if (web_contents) { + auto* delegate = + TabAndroid::FromWebContents(web_contents) + ? static_cast<android::TabWebContentsDelegateAndroid*>( + web_contents->GetDelegate()) + : nullptr; + is_custom_tab = delegate && delegate->IsCustomTab(); + } + auto delegate = std::make_unique<HeaderModificationDelegateImpl>( + profile, /*incognito_enabled=*/!is_custom_tab); +#else + auto delegate = std::make_unique<HeaderModificationDelegateImpl>(profile); +#endif + auto proxy = std::make_unique<ProxyingURLLoaderFactory>( + std::move(delegate), std::move(web_contents_getter), + std::move(receiver), std::move(target_factory), + base::BindOnce(&BrowserContextData::RemoveProxy, + self->weak_factory_.GetWeakPtr())); + self->proxies_.emplace(std::move(proxy)); + } + + void RemoveProxy(ProxyingURLLoaderFactory* proxy) { + auto it = proxies_.find(proxy); + DCHECK(it != proxies_.end()); + proxies_.erase(it); + } + + private: + BrowserContextData() {} + + std::set<std::unique_ptr<ProxyingURLLoaderFactory>, base::UniquePtrComparator> + proxies_; + + base::WeakPtrFactory<BrowserContextData> weak_factory_{this}; +}; + +} // namespace + +class ProxyingURLLoaderFactory::InProgressRequest + : public network::mojom::URLLoader, + public network::mojom::URLLoaderClient, + public base::SupportsUserData { + public: + InProgressRequest( + ProxyingURLLoaderFactory* factory, + mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& request, + mojo::PendingRemote<network::mojom::URLLoaderClient> client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation); + + InProgressRequest(const InProgressRequest&) = delete; + InProgressRequest& operator=(const InProgressRequest&) = delete; + + ~InProgressRequest() override { + if (destruction_callback_) + std::move(destruction_callback_).Run(); + } + + // network::mojom::URLLoader: + void FollowRedirect( + const std::vector<std::string>& removed_headers, + const net::HttpRequestHeaders& modified_headers, + const net::HttpRequestHeaders& modified_cors_exempt_headers, + const absl::optional<GURL>& new_url) override; + + void SetPriority(net::RequestPriority priority, + int32_t intra_priority_value) override { + target_loader_->SetPriority(priority, intra_priority_value); + } + + void PauseReadingBodyFromNet() override { + target_loader_->PauseReadingBodyFromNet(); + } + + void ResumeReadingBodyFromNet() override { + target_loader_->ResumeReadingBodyFromNet(); + } + + // network::mojom::URLLoaderClient: + void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override { + target_client_->OnReceiveEarlyHints(std::move(early_hints)); + } + void OnReceiveResponse(network::mojom::URLResponseHeadPtr head) override; + void OnReceiveRedirect(const net::RedirectInfo& redirect_info, + network::mojom::URLResponseHeadPtr head) override; + + void OnUploadProgress(int64_t current_position, + int64_t total_size, + OnUploadProgressCallback callback) override { + target_client_->OnUploadProgress(current_position, total_size, + std::move(callback)); + } + + void OnReceiveCachedMetadata(mojo_base::BigBuffer data) override { + target_client_->OnReceiveCachedMetadata(std::move(data)); + } + + void OnTransferSizeUpdated(int32_t transfer_size_diff) override { + target_client_->OnTransferSizeUpdated(transfer_size_diff); + } + + void OnStartLoadingResponseBody( + mojo::ScopedDataPipeConsumerHandle body) override { + target_client_->OnStartLoadingResponseBody(std::move(body)); + } + + void OnComplete(const network::URLLoaderCompletionStatus& status) override { + target_client_->OnComplete(status); + } + + private: + class ProxyRequestAdapter; + class ProxyResponseAdapter; + + void OnBindingsClosed() { + // Destroys |this|. + factory_->RemoveRequest(this); + } + + // Back pointer to the factory which owns this class. + const raw_ptr<ProxyingURLLoaderFactory> factory_; + + // Information about the current request. + GURL request_url_; + GURL response_url_; + GURL referrer_origin_; + net::HttpRequestHeaders headers_; + net::HttpRequestHeaders cors_exempt_headers_; + net::RedirectInfo redirect_info_; + const network::mojom::RequestDestination request_destination_; + const bool is_main_frame_; + const bool is_fetch_like_api_; + + base::OnceClosure destruction_callback_; + + // Messages received by |client_receiver_| are forwarded to |target_client_|. + mojo::Receiver<network::mojom::URLLoaderClient> client_receiver_{this}; + mojo::Remote<network::mojom::URLLoaderClient> target_client_; + + // Messages received by |loader_receiver_| are forwarded to |target_loader_|. + mojo::Receiver<network::mojom::URLLoader> loader_receiver_; + mojo::Remote<network::mojom::URLLoader> target_loader_; +}; + +class ProxyingURLLoaderFactory::InProgressRequest::ProxyRequestAdapter + : public ChromeRequestAdapter { + public: + // Does not take |modified_cors_exempt_headers| just because we don't have a + // use-case to modify it in this class now. + ProxyRequestAdapter(InProgressRequest* in_progress_request, + const net::HttpRequestHeaders& original_headers, + net::HttpRequestHeaders* modified_headers, + std::vector<std::string>* removed_headers) + : ChromeRequestAdapter(in_progress_request->request_url_, + original_headers, + modified_headers, + removed_headers), + in_progress_request_(in_progress_request) { + DCHECK(in_progress_request_); + } + + ProxyRequestAdapter(const ProxyRequestAdapter&) = delete; + ProxyRequestAdapter& operator=(const ProxyRequestAdapter&) = delete; + + ~ProxyRequestAdapter() override = default; + + content::WebContents::Getter GetWebContentsGetter() const override { + return in_progress_request_->factory_->web_contents_getter_; + } + + network::mojom::RequestDestination GetRequestDestination() const override { + return in_progress_request_->request_destination_; + } + + bool IsFetchLikeAPI() const override { + return in_progress_request_->is_fetch_like_api_; + } + + GURL GetReferrerOrigin() const override { + return in_progress_request_->referrer_origin_; + } + + void SetDestructionCallback(base::OnceClosure closure) override { + if (!in_progress_request_->destruction_callback_) + in_progress_request_->destruction_callback_ = std::move(closure); + } + + private: + const raw_ptr<InProgressRequest> in_progress_request_; +}; + +class ProxyingURLLoaderFactory::InProgressRequest::ProxyResponseAdapter + : public ResponseAdapter { + public: + ProxyResponseAdapter(InProgressRequest* in_progress_request, + net::HttpResponseHeaders* headers) + : in_progress_request_(in_progress_request), headers_(headers) { + DCHECK(in_progress_request_); + DCHECK(headers_); + } + + ProxyResponseAdapter(const ProxyResponseAdapter&) = delete; + ProxyResponseAdapter& operator=(const ProxyResponseAdapter&) = delete; + + ~ProxyResponseAdapter() override = default; + + // signin::ResponseAdapter + content::WebContents::Getter GetWebContentsGetter() const override { + return in_progress_request_->factory_->web_contents_getter_; + } + + bool IsMainFrame() const override { + return in_progress_request_->is_main_frame_; + } + + GURL GetOrigin() const override { + return in_progress_request_->response_url_.DeprecatedGetOriginAsURL(); + } + + const net::HttpResponseHeaders* GetHeaders() const override { + return headers_; + } + + void RemoveHeader(const std::string& name) override { + headers_->RemoveHeader(name); + } + + base::SupportsUserData::Data* GetUserData(const void* key) const override { + return in_progress_request_->GetUserData(key); + } + + void SetUserData( + const void* key, + std::unique_ptr<base::SupportsUserData::Data> data) override { + in_progress_request_->SetUserData(key, std::move(data)); + } + + private: + const raw_ptr<InProgressRequest> in_progress_request_; + const raw_ptr<net::HttpResponseHeaders> headers_; +}; + +ProxyingURLLoaderFactory::InProgressRequest::InProgressRequest( + ProxyingURLLoaderFactory* factory, + mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& request, + mojo::PendingRemote<network::mojom::URLLoaderClient> client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) + : factory_(factory), + request_url_(request.url), + response_url_(request.url), + referrer_origin_(request.referrer.DeprecatedGetOriginAsURL()), + request_destination_(request.destination), + is_main_frame_(request.is_main_frame), + is_fetch_like_api_(request.is_fetch_like_api), + target_client_(std::move(client)), + loader_receiver_(this, std::move(loader_receiver)) { + mojo::PendingRemote<network::mojom::URLLoaderClient> proxy_client = + client_receiver_.BindNewPipeAndPassRemote(); + + net::HttpRequestHeaders modified_headers; + std::vector<std::string> removed_headers; + ProxyRequestAdapter adapter(this, request.headers, &modified_headers, + &removed_headers); + factory_->delegate_->ProcessRequest(&adapter, GURL() /* redirect_url */); + + if (modified_headers.IsEmpty() && removed_headers.empty()) { + factory_->target_factory_->CreateLoaderAndStart( + target_loader_.BindNewPipeAndPassReceiver(), request_id, options, + request, std::move(proxy_client), traffic_annotation); + + // We need to keep a full copy of the request headers in case there is a + // redirect and the request headers need to be modified again. + headers_.CopyFrom(request.headers); + cors_exempt_headers_.CopyFrom(request.cors_exempt_headers); + } else { + network::ResourceRequest request_copy = request; + request_copy.headers.MergeFrom(modified_headers); + for (const std::string& name : removed_headers) { + request_copy.headers.RemoveHeader(name); + request_copy.cors_exempt_headers.RemoveHeader(name); + } + + factory_->target_factory_->CreateLoaderAndStart( + target_loader_.BindNewPipeAndPassReceiver(), request_id, options, + request_copy, std::move(proxy_client), traffic_annotation); + + headers_.Swap(&request_copy.headers); + cors_exempt_headers_.Swap(&request_copy.cors_exempt_headers); + } + + base::RepeatingClosure closure = base::BarrierClosure( + 2, base::BindOnce(&InProgressRequest::OnBindingsClosed, + base::Unretained(this))); + loader_receiver_.set_disconnect_handler(closure); + client_receiver_.set_disconnect_handler(closure); +} + +void ProxyingURLLoaderFactory::InProgressRequest::FollowRedirect( + const std::vector<std::string>& removed_headers_ext, + const net::HttpRequestHeaders& modified_headers_ext, + const net::HttpRequestHeaders& modified_cors_exempt_headers_ext, + const absl::optional<GURL>& opt_new_url) { + std::vector<std::string> removed_headers = removed_headers_ext; + net::HttpRequestHeaders modified_headers = modified_headers_ext; + net::HttpRequestHeaders modified_cors_exempt_headers = + modified_cors_exempt_headers_ext; + ProxyRequestAdapter adapter(this, headers_, &modified_headers, + &removed_headers); + factory_->delegate_->ProcessRequest(&adapter, redirect_info_.new_url); + + headers_.MergeFrom(modified_headers); + cors_exempt_headers_.MergeFrom(modified_cors_exempt_headers); + for (const std::string& name : removed_headers) { + headers_.RemoveHeader(name); + cors_exempt_headers_.RemoveHeader(name); + } + + target_loader_->FollowRedirect(removed_headers, modified_headers, + modified_cors_exempt_headers, opt_new_url); + + request_url_ = redirect_info_.new_url; + referrer_origin_ = + GURL(redirect_info_.new_referrer).DeprecatedGetOriginAsURL(); +} + +void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveResponse( + network::mojom::URLResponseHeadPtr head) { + // Even though |head| is const we can get a non-const pointer to the headers + // and modifications we made are passed to the target client. + ProxyResponseAdapter adapter(this, head->headers.get()); + factory_->delegate_->ProcessResponse(&adapter, GURL() /* redirect_url */); + target_client_->OnReceiveResponse(std::move(head)); +} + +void ProxyingURLLoaderFactory::InProgressRequest::OnReceiveRedirect( + const net::RedirectInfo& redirect_info, + network::mojom::URLResponseHeadPtr head) { + // Even though |head| is const we can get a non-const pointer to the headers + // and modifications we made are passed to the target client. + ProxyResponseAdapter adapter(this, head->headers.get()); + factory_->delegate_->ProcessResponse(&adapter, redirect_info.new_url); + target_client_->OnReceiveRedirect(redirect_info, std::move(head)); + + // The request URL returned by ProxyResponseAdapter::GetOrigin() is updated + // immediately but the URL and referrer + redirect_info_ = redirect_info; + response_url_ = redirect_info.new_url; +} + +ProxyingURLLoaderFactory::ProxyingURLLoaderFactory( + std::unique_ptr<HeaderModificationDelegate> delegate, + content::WebContents::Getter web_contents_getter, + mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver, + mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory, + DisconnectCallback on_disconnect) { + DCHECK(proxy_receivers_.empty()); + DCHECK(!target_factory_.is_bound()); + DCHECK(!delegate_); + DCHECK(!web_contents_getter_); + DCHECK(!on_disconnect_); + + delegate_ = std::move(delegate); + web_contents_getter_ = std::move(web_contents_getter); + on_disconnect_ = std::move(on_disconnect); + + target_factory_.Bind(std::move(target_factory)); + target_factory_.set_disconnect_handler(base::BindOnce( + &ProxyingURLLoaderFactory::OnTargetFactoryError, base::Unretained(this))); + + proxy_receivers_.Add(this, std::move(loader_receiver)); + proxy_receivers_.set_disconnect_handler(base::BindRepeating( + &ProxyingURLLoaderFactory::OnProxyBindingError, base::Unretained(this))); +} + +ProxyingURLLoaderFactory::~ProxyingURLLoaderFactory() = default; + +// static +bool ProxyingURLLoaderFactory::MaybeProxyRequest( + content::RenderFrameHost* render_frame_host, + bool is_navigation, + const url::Origin& request_initiator, + mojo::PendingReceiver<network::mojom::URLLoaderFactory>* factory_receiver) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + // Navigation requests are handled using signin::URLLoaderThrottle. + if (is_navigation) + return false; + + if (!render_frame_host) + return false; + + // This proxy should only be installed for subresource requests from a frame + // that is rendering the GAIA signon realm. + if (!gaia::IsGaiaSignonRealm(request_initiator.GetURL())) + return false; + + auto* web_contents = + content::WebContents::FromRenderFrameHost(render_frame_host); + auto* profile = + Profile::FromBrowserContext(web_contents->GetBrowserContext()); + if (profile->IsOffTheRecord()) + return false; + +#if BUILDFLAG(ENABLE_EXTENSIONS) + // Most requests from guest web views are ignored. + if (HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest( + web_contents)) { + return false; + } +#endif + + auto proxied_receiver = std::move(*factory_receiver); + // TODO(crbug.com/955171): Replace this with PendingRemote. + mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory_remote; + *factory_receiver = target_factory_remote.InitWithNewPipeAndPassReceiver(); + + auto web_contents_getter = + base::BindRepeating(&content::WebContents::FromFrameTreeNodeId, + render_frame_host->GetFrameTreeNodeId()); + + BrowserContextData::StartProxying(profile, std::move(web_contents_getter), + std::move(proxied_receiver), + std::move(target_factory_remote)); + return true; +} + +void ProxyingURLLoaderFactory::CreateLoaderAndStart( + mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& request, + mojo::PendingRemote<network::mojom::URLLoaderClient> client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) { + requests_.insert(std::make_unique<InProgressRequest>( + this, std::move(loader_receiver), request_id, options, request, + std::move(client), traffic_annotation)); +} + +void ProxyingURLLoaderFactory::Clone( + mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver) { + proxy_receivers_.Add(this, std::move(loader_receiver)); +} + +void ProxyingURLLoaderFactory::OnTargetFactoryError() { + // Stop calls to CreateLoaderAndStart() when |target_factory_| is invalid. + target_factory_.reset(); + proxy_receivers_.Clear(); + + MaybeDestroySelf(); +} + +void ProxyingURLLoaderFactory::OnProxyBindingError() { + if (proxy_receivers_.empty()) + target_factory_.reset(); + + MaybeDestroySelf(); +} + +void ProxyingURLLoaderFactory::RemoveRequest(InProgressRequest* request) { + auto it = requests_.find(request); + DCHECK(it != requests_.end()); + requests_.erase(it); + + MaybeDestroySelf(); +} + +void ProxyingURLLoaderFactory::MaybeDestroySelf() { + // Even if all URLLoaderFactory pipes connected to this object have been + // closed it has to stay alive until all active requests have completed. + if (target_factory_.is_bound() || !requests_.empty()) + return; + + // Deletes |this|. + std::move(on_disconnect_).Run(this); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h new file mode 100644 index 00000000000..28589803472 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h @@ -0,0 +1,100 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_ + +#include "base/callback.h" +#include "base/containers/unique_ptr_adapters.h" +#include "base/memory/ref_counted_delete_on_sequence.h" +#include "content/public/browser/web_contents.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver_set.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" + +#include <memory> +#include <set> + +namespace content { +class RenderFrameHost; +} + +namespace signin { + +class HeaderModificationDelegate; + +// This class is used to modify sub-resource requests made by the renderer +// that is displaying the GAIA signin realm, to the GAIA signin realm. When +// such a request is made a proxy is inserted between the renderer and the +// Network Service to modify request and response headers. +class ProxyingURLLoaderFactory : public network::mojom::URLLoaderFactory { + public: + using DisconnectCallback = + base::OnceCallback<void(ProxyingURLLoaderFactory*)>; + + // Constructor public for testing purposes. New instances should be created + // by calling MaybeProxyRequest(). + ProxyingURLLoaderFactory( + std::unique_ptr<HeaderModificationDelegate> delegate, + content::WebContents::Getter web_contents_getter, + mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver, + mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory, + DisconnectCallback on_disconnect); + + ProxyingURLLoaderFactory(const ProxyingURLLoaderFactory&) = delete; + ProxyingURLLoaderFactory& operator=(const ProxyingURLLoaderFactory&) = delete; + + ~ProxyingURLLoaderFactory() override; + + // Called when a renderer needs a URLLoaderFactory to give this module the + // opportunity to install a proxy. This is only done when + // https://accounts.google.com is loaded in non-incognito mode. Returns true + // when |factory_request| has been proxied. + static bool MaybeProxyRequest( + content::RenderFrameHost* render_frame_host, + bool is_navigation, + const url::Origin& request_initiator, + mojo::PendingReceiver<network::mojom::URLLoaderFactory>* + factory_receiver); + + // network::mojom::URLLoaderFactory: + void CreateLoaderAndStart( + mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& request, + mojo::PendingRemote<network::mojom::URLLoaderClient> client, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) + override; + void Clone(mojo::PendingReceiver<network::mojom::URLLoaderFactory> + loader_receiver) override; + + private: + friend class base::DeleteHelper<ProxyingURLLoaderFactory>; + friend class base::RefCountedDeleteOnSequence<ProxyingURLLoaderFactory>; + + class InProgressRequest; + class ProxyRequestAdapter; + class ProxyResponseAdapter; + + void OnTargetFactoryError(); + void OnProxyBindingError(); + void RemoveRequest(InProgressRequest* request); + void MaybeDestroySelf(); + + std::unique_ptr<HeaderModificationDelegate> delegate_; + content::WebContents::Getter web_contents_getter_; + + mojo::ReceiverSet<network::mojom::URLLoaderFactory> proxy_receivers_; + std::set<std::unique_ptr<InProgressRequest>, base::UniquePtrComparator> + requests_; + mojo::Remote<network::mojom::URLLoaderFactory> target_factory_; + DisconnectCallback on_disconnect_; +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_PROXYING_URL_LOADER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc new file mode 100644 index 00000000000..2d9772cc070 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_proxying_url_loader_factory_unittest.cc @@ -0,0 +1,359 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h" + +#include <algorithm> +#include <memory> + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/run_loop.h" +#include "base/test/mock_callback.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "content/public/test/browser_task_environment.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" +#include "services/network/public/mojom/url_response_head.mojom.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::Invoke; +using testing::_; + +namespace signin { + +namespace { + +class MockDelegate : public HeaderModificationDelegate { + public: + MockDelegate() = default; + + MockDelegate(const MockDelegate&) = delete; + MockDelegate& operator=(const MockDelegate&) = delete; + + ~MockDelegate() override = default; + + MOCK_METHOD1(ShouldInterceptNavigation, bool(content::WebContents* contents)); + MOCK_METHOD2(ProcessRequest, + void(ChromeRequestAdapter* request_adapter, + const GURL& redirect_url)); + MOCK_METHOD2(ProcessResponse, + void(ResponseAdapter* response_adapter, + const GURL& redirect_url)); + + base::WeakPtr<MockDelegate> GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + + private: + base::WeakPtrFactory<MockDelegate> weak_factory_{this}; +}; + +content::WebContents::Getter NullWebContentsGetter() { + return base::BindRepeating([]() -> content::WebContents* { return nullptr; }); +} + +} // namespace + +class ChromeSigninProxyingURLLoaderFactoryTest : public testing::Test { + public: + ChromeSigninProxyingURLLoaderFactoryTest() + : test_factory_receiver_(&test_factory_) {} + + ChromeSigninProxyingURLLoaderFactoryTest( + const ChromeSigninProxyingURLLoaderFactoryTest&) = delete; + ChromeSigninProxyingURLLoaderFactoryTest& operator=( + const ChromeSigninProxyingURLLoaderFactoryTest&) = delete; + + ~ChromeSigninProxyingURLLoaderFactoryTest() override {} + + base::WeakPtr<MockDelegate> StartRequest( + std::unique_ptr<network::ResourceRequest> request) { + loader_ = network::SimpleURLLoader::Create(std::move(request), + TRAFFIC_ANNOTATION_FOR_TESTS); + + mojo::Remote<network::mojom::URLLoaderFactory> factory_remote; + auto factory_request = factory_remote.BindNewPipeAndPassReceiver(); + loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( + factory_remote.get(), + base::BindOnce( + &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete, + base::Unretained(this))); + + auto delegate = std::make_unique<MockDelegate>(); + base::WeakPtr<MockDelegate> delegate_weak = delegate->GetWeakPtr(); + + proxying_factory_ = std::make_unique<ProxyingURLLoaderFactory>( + std::move(delegate), NullWebContentsGetter(), + std::move(factory_request), + test_factory_receiver_.BindNewPipeAndPassRemote(), + base::BindOnce(&ChromeSigninProxyingURLLoaderFactoryTest::OnDisconnect, + base::Unretained(this))); + + return delegate_weak; + } + + void CloseFactoryReceiver() { test_factory_receiver_.reset(); } + + network::TestURLLoaderFactory* factory() { return &test_factory_; } + network::SimpleURLLoader* loader() { return loader_.get(); } + std::string* response_body() { return response_body_.get(); } + + void OnDownloadComplete(std::unique_ptr<std::string> body) { + response_body_ = std::move(body); + } + + private: + void OnDisconnect(ProxyingURLLoaderFactory* factory) { + EXPECT_EQ(factory, proxying_factory_.get()); + proxying_factory_.reset(); + } + + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr<network::SimpleURLLoader> loader_; + std::unique_ptr<ProxyingURLLoaderFactory> proxying_factory_; + network::TestURLLoaderFactory test_factory_; + mojo::Receiver<network::mojom::URLLoaderFactory> test_factory_receiver_; + std::unique_ptr<std::string> response_body_; +}; + +TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, NoModification) { + auto request = std::make_unique<network::ResourceRequest>(); + request->url = GURL("https://google.com/"); + + factory()->AddResponse("https://google.com/", "Hello."); + base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request)); + + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(net::OK, loader()->NetError()); + ASSERT_TRUE(response_body()); + EXPECT_EQ("Hello.", *response_body()); +} + +TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, ModifyHeaders) { + const GURL kTestURL("https://google.com/index.html"); + const GURL kTestReferrer("https://chrome.com/referrer.html"); + const GURL kTestRedirectURL("https://youtube.com/index.html"); + + // Set up the request. + auto request = std::make_unique<network::ResourceRequest>(); + request->url = kTestURL; + request->referrer = kTestReferrer; + request->destination = network::mojom::RequestDestination::kDocument; + request->is_main_frame = true; + request->headers.SetHeader("X-Request-1", "Foo"); + + base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request)); + + // The first destruction callback added by ProcessRequest is expected to be + // called. The second (added after a redirect) will not be. + base::MockCallback<base::OnceClosure> destruction_callback; + EXPECT_CALL(destruction_callback, Run()).Times(1); + base::MockCallback<base::OnceClosure> ignored_destruction_callback; + EXPECT_CALL(ignored_destruction_callback, Run()).Times(0); + + // The delegate will be called twice to process a request, first when the + // request is started and again when the request is redirected. + EXPECT_CALL(*delegate, ProcessRequest(_, _)) + .WillOnce( + Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(kTestURL, adapter->GetUrl()); + EXPECT_EQ(network::mojom::RequestDestination::kDocument, + adapter->GetRequestDestination()); + EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); + + EXPECT_TRUE(adapter->HasHeader("X-Request-1")); + adapter->RemoveRequestHeaderByName("X-Request-1"); + EXPECT_FALSE(adapter->HasHeader("X-Request-1")); + + adapter->SetExtraHeaderByName("X-Request-2", "Bar"); + EXPECT_TRUE(adapter->HasHeader("X-Request-2")); + + EXPECT_EQ(GURL(), redirect_url); + + adapter->SetDestructionCallback(destruction_callback.Get()); + })) + .WillOnce( + Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(network::mojom::RequestDestination::kDocument, + adapter->GetRequestDestination()); + + // Changes to the URL and referrer take effect after the redirect + // is followed. + EXPECT_EQ(kTestURL, adapter->GetUrl()); + EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); + + // X-Request-1 and X-Request-2 were modified in the previous call to + // ProcessRequest(). These changes should still be present. + EXPECT_FALSE(adapter->HasHeader("X-Request-1")); + EXPECT_TRUE(adapter->HasHeader("X-Request-2")); + + adapter->RemoveRequestHeaderByName("X-Request-2"); + EXPECT_FALSE(adapter->HasHeader("X-Request-2")); + + adapter->SetExtraHeaderByName("X-Request-3", "Baz"); + EXPECT_TRUE(adapter->HasHeader("X-Request-3")); + + EXPECT_EQ(kTestRedirectURL, redirect_url); + + adapter->SetDestructionCallback(ignored_destruction_callback.Get()); + })); + + const void* const kResponseUserDataKey = &kResponseUserDataKey; + std::unique_ptr<base::SupportsUserData::Data> response_user_data = + std::make_unique<base::SupportsUserData::Data>(); + base::SupportsUserData::Data* response_user_data_ptr = + response_user_data.get(); + + // The delegate will also be called twice to process a response, first when + // the redirect is received and again for the redirect response. + EXPECT_CALL(*delegate, ProcessResponse(_, _)) + .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin()); + EXPECT_TRUE(adapter->IsMainFrame()); + + adapter->SetUserData(kResponseUserDataKey, + std::move(response_user_data)); + EXPECT_EQ(response_user_data_ptr, + adapter->GetUserData(kResponseUserDataKey)); + + const net::HttpResponseHeaders* headers = adapter->GetHeaders(); + EXPECT_TRUE(headers->HasHeader("X-Response-1")); + EXPECT_TRUE(headers->HasHeader("X-Response-2")); + adapter->RemoveHeader("X-Response-2"); + + EXPECT_EQ(kTestRedirectURL, redirect_url); + })) + .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin()); + EXPECT_TRUE(adapter->IsMainFrame()); + + EXPECT_EQ(response_user_data_ptr, + adapter->GetUserData(kResponseUserDataKey)); + + const net::HttpResponseHeaders* headers = adapter->GetHeaders(); + // This is a new response and so previous headers should not carry over. + EXPECT_FALSE(headers->HasHeader("X-Response-1")); + EXPECT_FALSE(headers->HasHeader("X-Response-2")); + + EXPECT_TRUE(headers->HasHeader("X-Response-3")); + EXPECT_TRUE(headers->HasHeader("X-Response-4")); + adapter->RemoveHeader("X-Response-3"); + + EXPECT_EQ(GURL(), redirect_url); + })); + + // Set up a redirect and final response. + { + net::RedirectInfo redirect_info; + redirect_info.new_url = kTestRedirectURL; + // An HTTPS to HTTPS redirect such as this wouldn't normally change the + // referrer but we do for testing purposes. + redirect_info.new_referrer = kTestURL.spec(); + + auto redirect_head = network::mojom::URLResponseHead::New(); + redirect_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(""); + redirect_head->headers->SetHeader("X-Response-1", "Foo"); + redirect_head->headers->SetHeader("X-Response-2", "Bar"); + + auto response_head = network::mojom::URLResponseHead::New(); + response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(""); + response_head->headers->SetHeader("X-Response-3", "Foo"); + response_head->headers->SetHeader("X-Response-4", "Bar"); + std::string body("Hello."); + network::URLLoaderCompletionStatus status; + status.decoded_body_length = body.size(); + + network::TestURLLoaderFactory::Redirects redirects; + redirects.push_back({redirect_info, std::move(redirect_head)}); + + factory()->AddResponse(kTestURL, std::move(response_head), body, status, + std::move(redirects)); + } + + // Wait for the request to complete and check the response. + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(net::OK, loader()->NetError()); + const network::mojom::URLResponseHead* response_head = + loader()->ResponseInfo(); + ASSERT_TRUE(response_head && response_head->headers); + EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3")); + EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4")); + ASSERT_TRUE(response_body()); + EXPECT_EQ("Hello.", *response_body()); + + // NOTE: TestURLLoaderFactory currently does not expose modifications to + // request headers and so we cannot verify that the modifications have been + // passed to the target URLLoader. +} + +TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, TargetFactoryFailure) { + mojo::Remote<network::mojom::URLLoaderFactory> factory_remote; + mojo::PendingRemote<network::mojom::URLLoaderFactory> + pending_target_factory_remote; + auto target_factory_receiver = + pending_target_factory_remote.InitWithNewPipeAndPassReceiver(); + + // Without a target factory the proxy will process no requests. + auto delegate = std::make_unique<MockDelegate>(); + EXPECT_CALL(*delegate, ProcessRequest(_, _)).Times(0); + + auto proxying_factory = std::make_unique<ProxyingURLLoaderFactory>( + std::move(delegate), NullWebContentsGetter(), + factory_remote.BindNewPipeAndPassReceiver(), + std::move(pending_target_factory_remote), base::DoNothing()); + + // Close |target_factory_receiver| instead of binding it to a + // URLLoaderFactory. Spin the message loop so that the connection error + // handler can run. + target_factory_receiver = mojo::NullReceiver(); + base::RunLoop().RunUntilIdle(); + + auto request = std::make_unique<network::ResourceRequest>(); + request->url = GURL("https://google.com"); + auto loader = network::SimpleURLLoader::Create(std::move(request), + TRAFFIC_ANNOTATION_FOR_TESTS); + loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( + factory_remote.get(), + base::BindOnce( + &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete, + base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(response_body()); + EXPECT_EQ(net::ERR_FAILED, loader->NetError()); +} + +TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, RequestKeepAlive) { + // Start the request. + auto request = std::make_unique<network::ResourceRequest>(); + request->url = GURL("https://google.com"); + base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request)); + base::RunLoop().RunUntilIdle(); + + // Close the factory receiver and spin the message loop again to allow the + // connection error handler to be called. + CloseFactoryReceiver(); + base::RunLoop().RunUntilIdle(); + + // The ProxyingURLLoaderFactory should not have been destroyed yet because + // there is still an in progress request that has not been completed. + EXPECT_TRUE(delegate); + + // Complete the request. + factory()->AddResponse("https://google.com", "Hello."); + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(delegate); + EXPECT_EQ(net::OK, loader()->NetError()); + ASSERT_TRUE(response_body()); + EXPECT_EQ("Hello.", *response_body()); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc new file mode 100644 index 00000000000..63fe0ae0a6b --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.cc @@ -0,0 +1,141 @@ +// 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. + +#include "chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h" + +#include <string> +#include <vector> + +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/signin/core/browser/signin_status_metrics_provider.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#endif + +ChromeSigninStatusMetricsProviderDelegate:: + ChromeSigninStatusMetricsProviderDelegate() {} + +ChromeSigninStatusMetricsProviderDelegate:: + ~ChromeSigninStatusMetricsProviderDelegate() { +#if !defined(OS_ANDROID) + BrowserList::RemoveObserver(this); +#endif + + auto* factory = IdentityManagerFactory::GetInstance(); + if (factory) + factory->RemoveObserver(this); +} + +void ChromeSigninStatusMetricsProviderDelegate::Initialize() { +#if !defined(OS_ANDROID) + // On Android, there is always only one profile in any situation, opening new + // windows (which is possible with only some Android devices) will not change + // the opened profiles signin status. + BrowserList::AddObserver(this); +#endif + + auto* factory = IdentityManagerFactory::GetInstance(); + if (factory) + factory->AddObserver(this); +} + +AccountsStatus +ChromeSigninStatusMetricsProviderDelegate::GetStatusOfAllAccounts() { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + std::vector<Profile*> profile_list = profile_manager->GetLoadedProfiles(); + + AccountsStatus accounts_status; + accounts_status.num_accounts = profile_list.size(); + for (Profile* profile : profile_list) { +#if !defined(OS_ANDROID) + if (chrome::GetBrowserCount(profile) == 0) { + // The profile is loaded, but there's no opened browser for this profile. + continue; + } +#endif + accounts_status.num_opened_accounts++; + + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile->GetOriginalProfile()); + if (identity_manager && + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + accounts_status.num_signed_in_accounts++; + } + } + + return accounts_status; +} + +std::vector<signin::IdentityManager*> +ChromeSigninStatusMetricsProviderDelegate::GetIdentityManagersForAllAccounts() { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + std::vector<Profile*> profiles = profile_manager->GetLoadedProfiles(); + + std::vector<signin::IdentityManager*> managers; + for (Profile* profile : profiles) { + auto* identity_manager = + IdentityManagerFactory::GetForProfileIfExists(profile); + if (identity_manager) + managers.push_back(identity_manager); + } + + return managers; +} + +#if !defined(OS_ANDROID) +void ChromeSigninStatusMetricsProviderDelegate::OnBrowserAdded( + Browser* browser) { + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(browser->profile()); + + // Nothing will change if the opened browser is in incognito mode. + if (!identity_manager) + return; + + const bool signed_in = + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync); + UpdateStatusWhenBrowserAdded(signed_in); +} +#endif + +void ChromeSigninStatusMetricsProviderDelegate::IdentityManagerCreated( + signin::IdentityManager* identity_manager) { + owner()->OnIdentityManagerCreated(identity_manager); +} + +void ChromeSigninStatusMetricsProviderDelegate::UpdateStatusWhenBrowserAdded( + bool signed_in) { +#if !defined(OS_ANDROID) + SigninStatusMetricsProviderBase::SigninStatus status = + owner()->signin_status(); + + // NOTE: If |status| is MIXED_SIGNIN_STATUS, this method + // intentionally does not update it. + if ((status == SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN && + signed_in) || + (status == SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN && + !signed_in)) { + owner()->UpdateSigninStatus( + SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS); + } else if (status == SigninStatusMetricsProviderBase::UNKNOWN_SIGNIN_STATUS) { + // If when function ProvideCurrentSessionData() is called, Chrome is + // running in the background with no browser window opened, |signin_status_| + // will be reset to |UNKNOWN_SIGNIN_STATUS|. Then this newly added browser + // is the only opened browser/profile and its signin status represents + // the whole status. + owner()->UpdateSigninStatus( + signed_in + ? SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN + : SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN); + } +#endif +} diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h new file mode 100644 index 00000000000..800d5f8e3f5 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h @@ -0,0 +1,58 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_ + +#include <vector> + +#include "base/gtest_prod_util.h" +#include "build/build_config.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/signin/core/browser/signin_status_metrics_provider_delegate.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/ui/browser_list_observer.h" +#endif // !defined(OS_ANDROID) + +class ChromeSigninStatusMetricsProviderDelegate + : public SigninStatusMetricsProviderDelegate, +#if !defined(OS_ANDROID) + public BrowserListObserver, +#endif + public IdentityManagerFactory::Observer { + public: + ChromeSigninStatusMetricsProviderDelegate(); + + ChromeSigninStatusMetricsProviderDelegate( + const ChromeSigninStatusMetricsProviderDelegate&) = delete; + ChromeSigninStatusMetricsProviderDelegate& operator=( + const ChromeSigninStatusMetricsProviderDelegate&) = delete; + + ~ChromeSigninStatusMetricsProviderDelegate() override; + + private: + FRIEND_TEST_ALL_PREFIXES(ChromeSigninStatusMetricsProviderDelegateTest, + UpdateStatusWhenBrowserAdded); + + // SigninStatusMetricsProviderDelegate: + void Initialize() override; + AccountsStatus GetStatusOfAllAccounts() override; + std::vector<signin::IdentityManager*> GetIdentityManagersForAllAccounts() + override; + +#if !defined(OS_ANDROID) + // BrowserListObserver: + void OnBrowserAdded(Browser* browser) override; +#endif + + // IdentityManagerFactoryObserver: + void IdentityManagerCreated( + signin::IdentityManager* identity_manager) override; + + // Updates the sign-in status right after a new browser is opened. + void UpdateStatusWhenBrowserAdded(bool signed_in); +}; + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_STATUS_METRICS_PROVIDER_DELEGATE_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc new file mode 100644 index 00000000000..fd83b3aada5 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_status_metrics_provider_delegate_unittest.cc @@ -0,0 +1,61 @@ +// Copyright 2014 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. + +#include "chrome/browser/signin/chrome_signin_status_metrics_provider_delegate.h" + +#include <utility> + +#include "build/build_config.h" +#include "components/signin/core/browser/signin_status_metrics_provider.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if !defined(OS_ANDROID) +TEST(ChromeSigninStatusMetricsProviderDelegateTest, + UpdateStatusWhenBrowserAdded) { + content::BrowserTaskEnvironment task_environment; + + std::unique_ptr<ChromeSigninStatusMetricsProviderDelegate> delegate( + new ChromeSigninStatusMetricsProviderDelegate); + ChromeSigninStatusMetricsProviderDelegate* raw_delegate = delegate.get(); + std::unique_ptr<SigninStatusMetricsProvider> metrics_provider = + SigninStatusMetricsProvider::CreateInstance(std::move(delegate)); + + // Initial status is all signed in and then a signed-in browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 2); + raw_delegate->UpdateStatusWhenBrowserAdded(true); + EXPECT_EQ(SigninStatusMetricsProviderBase::ALL_PROFILES_SIGNED_IN, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is all signed in and then a signed-out browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 2); + raw_delegate->UpdateStatusWhenBrowserAdded(false); + EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is all signed out and then a signed-in browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 0); + raw_delegate->UpdateStatusWhenBrowserAdded(true); + EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is all signed out and then a signed-out browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 0); + raw_delegate->UpdateStatusWhenBrowserAdded(false); + EXPECT_EQ(SigninStatusMetricsProviderBase::ALL_PROFILES_NOT_SIGNED_IN, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is mixed and then a signed-in browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 1); + raw_delegate->UpdateStatusWhenBrowserAdded(true); + EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS, + metrics_provider->GetSigninStatusForTesting()); + + // Initial status is mixed and then a signed-out browser is opened. + metrics_provider->UpdateInitialSigninStatusForTesting(2, 1); + raw_delegate->UpdateStatusWhenBrowserAdded(false); + EXPECT_EQ(SigninStatusMetricsProviderBase::MIXED_SIGNIN_STATUS, + metrics_provider->GetSigninStatusForTesting()); +} +#endif diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc new file mode 100644 index 00000000000..528fc7b6d44 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.cc @@ -0,0 +1,189 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/chrome_signin_url_loader_throttle.h" + +#include "base/memory/ptr_util.h" +#include "base/memory/raw_ptr.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/url_response_head.mojom.h" + +namespace signin { + +class URLLoaderThrottle::ThrottleRequestAdapter : public ChromeRequestAdapter { + public: + ThrottleRequestAdapter(URLLoaderThrottle* throttle, + const net::HttpRequestHeaders& original_headers, + net::HttpRequestHeaders* modified_headers, + std::vector<std::string>* headers_to_remove) + : ChromeRequestAdapter(throttle->request_url_, + original_headers, + modified_headers, + headers_to_remove), + throttle_(throttle) {} + + ThrottleRequestAdapter(const ThrottleRequestAdapter&) = delete; + ThrottleRequestAdapter& operator=(const ThrottleRequestAdapter&) = delete; + + ~ThrottleRequestAdapter() override = default; + + // ChromeRequestAdapter + content::WebContents::Getter GetWebContentsGetter() const override { + return throttle_->web_contents_getter_; + } + + network::mojom::RequestDestination GetRequestDestination() const override { + return throttle_->request_destination_; + } + + bool IsFetchLikeAPI() const override { + return throttle_->request_is_fetch_like_api_; + } + + GURL GetReferrerOrigin() const override { + return throttle_->request_referrer_.DeprecatedGetOriginAsURL(); + } + + void SetDestructionCallback(base::OnceClosure closure) override { + if (!throttle_->destruction_callback_) + throttle_->destruction_callback_ = std::move(closure); + } + + private: + const raw_ptr<URLLoaderThrottle> throttle_; +}; + +class URLLoaderThrottle::ThrottleResponseAdapter : public ResponseAdapter { + public: + ThrottleResponseAdapter(URLLoaderThrottle* throttle, + net::HttpResponseHeaders* headers) + : throttle_(throttle), headers_(headers) {} + + ThrottleResponseAdapter(const ThrottleResponseAdapter&) = delete; + ThrottleResponseAdapter& operator=(const ThrottleResponseAdapter&) = delete; + + ~ThrottleResponseAdapter() override = default; + + // ResponseAdapter + content::WebContents::Getter GetWebContentsGetter() const override { + return throttle_->web_contents_getter_; + } + + bool IsMainFrame() const override { + return throttle_->request_destination_ == + network::mojom::RequestDestination::kDocument; + } + + GURL GetOrigin() const override { + return throttle_->request_url_.DeprecatedGetOriginAsURL(); + } + + const net::HttpResponseHeaders* GetHeaders() const override { + return headers_; + } + + void RemoveHeader(const std::string& name) override { + headers_->RemoveHeader(name); + } + + base::SupportsUserData::Data* GetUserData(const void* key) const override { + return throttle_->GetUserData(key); + } + + void SetUserData( + const void* key, + std::unique_ptr<base::SupportsUserData::Data> data) override { + throttle_->SetUserData(key, std::move(data)); + } + + private: + const raw_ptr<URLLoaderThrottle> throttle_; + raw_ptr<net::HttpResponseHeaders> headers_; +}; + +// static +std::unique_ptr<URLLoaderThrottle> URLLoaderThrottle::MaybeCreate( + std::unique_ptr<HeaderModificationDelegate> delegate, + content::WebContents::Getter web_contents_getter) { + if (!delegate->ShouldInterceptNavigation(web_contents_getter.Run())) + return nullptr; + + return base::WrapUnique(new URLLoaderThrottle( + std::move(delegate), std::move(web_contents_getter))); +} + +URLLoaderThrottle::~URLLoaderThrottle() { + if (destruction_callback_) + std::move(destruction_callback_).Run(); +} + +void URLLoaderThrottle::WillStartRequest(network::ResourceRequest* request, + bool* defer) { + request_url_ = request->url; + request_referrer_ = request->referrer; + request_destination_ = request->destination; + request_is_fetch_like_api_ = request->is_fetch_like_api; + + net::HttpRequestHeaders modified_request_headers; + std::vector<std::string> to_be_removed_request_headers; + + ThrottleRequestAdapter adapter(this, request->headers, + &modified_request_headers, + &to_be_removed_request_headers); + delegate_->ProcessRequest(&adapter, GURL() /* redirect_url */); + + request->headers.MergeFrom(modified_request_headers); + for (const std::string& name : to_be_removed_request_headers) + request->headers.RemoveHeader(name); + + // We need to keep a full copy of the request headers for later calls to + // FixAccountConsistencyRequestHeader. Perhaps this could be replaced with + // more specific per-request state. + request_headers_.CopyFrom(request->headers); + request_cors_exempt_headers_.CopyFrom(request->cors_exempt_headers); +} + +void URLLoaderThrottle::WillRedirectRequest( + net::RedirectInfo* redirect_info, + const network::mojom::URLResponseHead& response_head, + bool* /* defer */, + std::vector<std::string>* to_be_removed_request_headers, + net::HttpRequestHeaders* modified_request_headers, + net::HttpRequestHeaders* modified_cors_exempt_request_headers) { + ThrottleRequestAdapter request_adapter(this, request_headers_, + modified_request_headers, + to_be_removed_request_headers); + delegate_->ProcessRequest(&request_adapter, redirect_info->new_url); + + request_headers_.MergeFrom(*modified_request_headers); + for (const std::string& name : *to_be_removed_request_headers) + request_headers_.RemoveHeader(name); + + // Modifications to |response_head.headers| will be passed to the + // URLLoaderClient even though |response_head| is const. + ThrottleResponseAdapter response_adapter(this, response_head.headers.get()); + delegate_->ProcessResponse(&response_adapter, redirect_info->new_url); + + request_url_ = redirect_info->new_url; + request_referrer_ = GURL(redirect_info->new_referrer); +} + +void URLLoaderThrottle::WillProcessResponse( + const GURL& response_url, + network::mojom::URLResponseHead* response_head, + bool* defer) { + ThrottleResponseAdapter adapter(this, response_head->headers.get()); + delegate_->ProcessResponse(&adapter, GURL() /* redirect_url */); +} + +URLLoaderThrottle::URLLoaderThrottle( + std::unique_ptr<HeaderModificationDelegate> delegate, + content::WebContents::Getter web_contents_getter) + : delegate_(std::move(delegate)), + web_contents_getter_(std::move(web_contents_getter)) {} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h new file mode 100644 index 00000000000..352a84291d7 --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle.h @@ -0,0 +1,72 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_ +#define CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_ + +#include "base/supports_user_data.h" +#include "content/public/browser/web_contents.h" +#include "net/http/http_request_headers.h" +#include "services/network/public/mojom/fetch_api.mojom-shared.h" +#include "third_party/blink/public/common/loader/url_loader_throttle.h" + +namespace signin { + +class HeaderModificationDelegate; + +// This class is used to modify the main frame request made when loading the +// GAIA signin realm. +class URLLoaderThrottle : public blink::URLLoaderThrottle, + public base::SupportsUserData { + public: + // Creates a new throttle if |delegate| says that this request should be + // intercepted. + static std::unique_ptr<URLLoaderThrottle> MaybeCreate( + std::unique_ptr<HeaderModificationDelegate> delegate, + content::WebContents::Getter web_contents_getter); + + URLLoaderThrottle(const URLLoaderThrottle&) = delete; + URLLoaderThrottle& operator=(const URLLoaderThrottle&) = delete; + + ~URLLoaderThrottle() override; + + // blink::URLLoaderThrottle + void WillStartRequest(network::ResourceRequest* request, + bool* defer) override; + void WillRedirectRequest( + net::RedirectInfo* redirect_info, + const network::mojom::URLResponseHead& response_head, + bool* defer, + std::vector<std::string>* headers_to_remove, + net::HttpRequestHeaders* modified_headers, + net::HttpRequestHeaders* modified_cors_exempt_headers) override; + void WillProcessResponse(const GURL& response_url, + network::mojom::URLResponseHead* response_head, + bool* defer) override; + + private: + class ThrottleRequestAdapter; + class ThrottleResponseAdapter; + + URLLoaderThrottle(std::unique_ptr<HeaderModificationDelegate> delegate, + content::WebContents::Getter web_contents_getter); + + const std::unique_ptr<HeaderModificationDelegate> delegate_; + const content::WebContents::Getter web_contents_getter_; + + // Information about the current request. + GURL request_url_; + GURL request_referrer_; + net::HttpRequestHeaders request_headers_; + net::HttpRequestHeaders request_cors_exempt_headers_; + network::mojom::RequestDestination request_destination_ = + network::mojom::RequestDestination::kEmpty; + bool request_is_fetch_like_api_ = false; + + base::OnceClosure destruction_callback_; +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_CHROME_SIGNIN_URL_LOADER_THROTTLE_H_ diff --git a/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc new file mode 100644 index 00000000000..f28486ee18a --- /dev/null +++ b/chromium/chrome/browser/signin/chrome_signin_url_loader_throttle_unittest.cc @@ -0,0 +1,281 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/chrome_signin_url_loader_throttle.h" + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/test/mock_callback.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/mojom/url_response_head.mojom.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::ElementsAre; +using testing::Invoke; +using testing::Return; +using testing::_; + +namespace signin { + +namespace { + +class MockDelegate : public HeaderModificationDelegate { + public: + MockDelegate() = default; + + MockDelegate(const MockDelegate&) = delete; + MockDelegate& operator=(const MockDelegate&) = delete; + + ~MockDelegate() override = default; + + MOCK_METHOD1(ShouldInterceptNavigation, bool(content::WebContents* contents)); + MOCK_METHOD2(ProcessRequest, + void(ChromeRequestAdapter* request_adapter, + const GURL& redirect_url)); + MOCK_METHOD2(ProcessResponse, + void(ResponseAdapter* response_adapter, + const GURL& redirect_url)); +}; + +content::WebContents::Getter NullWebContentsGetter() { + return base::BindRepeating([]() -> content::WebContents* { return nullptr; }); +} + +} // namespace + +TEST(ChromeSigninURLLoaderThrottleTest, NoIntercept) { + auto* delegate = new MockDelegate(); + + EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(false)); + EXPECT_FALSE(URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate), + NullWebContentsGetter())); +} + +TEST(ChromeSigninURLLoaderThrottleTest, Intercept) { + auto* delegate = new MockDelegate(); + EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(true)); + auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate), + NullWebContentsGetter()); + ASSERT_TRUE(throttle); + + // Phase 1: Start the request. + + const GURL kTestURL("https://google.com/index.html"); + const GURL kTestReferrer("https://chrome.com/referrer.html"); + base::MockCallback<base::OnceClosure> destruction_callback; + EXPECT_CALL(*delegate, ProcessRequest(_, _)) + .WillOnce( + Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(kTestURL, adapter->GetUrl()); + EXPECT_EQ(network::mojom::RequestDestination::kDocument, + adapter->GetRequestDestination()); + EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); + + EXPECT_TRUE(adapter->HasHeader("X-Request-1")); + adapter->RemoveRequestHeaderByName("X-Request-1"); + EXPECT_FALSE(adapter->HasHeader("X-Request-1")); + + adapter->SetExtraHeaderByName("X-Request-2", "Bar"); + EXPECT_TRUE(adapter->HasHeader("X-Request-2")); + + EXPECT_EQ(GURL(), redirect_url); + + adapter->SetDestructionCallback(destruction_callback.Get()); + })); + + network::ResourceRequest request; + request.url = kTestURL; + request.referrer = kTestReferrer; + request.destination = network::mojom::RequestDestination::kDocument; + request.headers.SetHeader("X-Request-1", "Foo"); + bool defer = false; + throttle->WillStartRequest(&request, &defer); + + EXPECT_FALSE(request.headers.HasHeader("X-Request-1")); + std::string value; + EXPECT_TRUE(request.headers.GetHeader("X-Request-2", &value)); + EXPECT_EQ("Bar", value); + + EXPECT_FALSE(defer); + + testing::Mock::VerifyAndClearExpectations(delegate); + + // Phase 2: Redirect the request. + + const GURL kTestRedirectURL("https://youtube.com/index.html"); + const void* const kResponseUserDataKey = &kResponseUserDataKey; + std::unique_ptr<base::SupportsUserData::Data> response_user_data = + std::make_unique<base::SupportsUserData::Data>(); + base::SupportsUserData::Data* response_user_data_ptr = + response_user_data.get(); + + EXPECT_CALL(*delegate, ProcessResponse(_, _)) + .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin()); + EXPECT_TRUE(adapter->IsMainFrame()); + + adapter->SetUserData(kResponseUserDataKey, + std::move(response_user_data)); + EXPECT_EQ(response_user_data_ptr, + adapter->GetUserData(kResponseUserDataKey)); + + const net::HttpResponseHeaders* headers = adapter->GetHeaders(); + EXPECT_TRUE(headers->HasHeader("X-Response-1")); + EXPECT_TRUE(headers->HasHeader("X-Response-2")); + adapter->RemoveHeader("X-Response-2"); + + EXPECT_EQ(kTestRedirectURL, redirect_url); + })); + + base::MockCallback<base::OnceClosure> ignored_destruction_callback; + EXPECT_CALL(*delegate, ProcessRequest(_, _)) + .WillOnce( + Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(network::mojom::RequestDestination::kDocument, + adapter->GetRequestDestination()); + + // Changes to the URL and referrer take effect after the redirect + // is followed. + EXPECT_EQ(kTestURL, adapter->GetUrl()); + EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); + + // X-Request-1 and X-Request-2 were modified in the previous call to + // ProcessRequest(). These changes should still be present. + EXPECT_FALSE(adapter->HasHeader("X-Request-1")); + EXPECT_TRUE(adapter->HasHeader("X-Request-2")); + + adapter->RemoveRequestHeaderByName("X-Request-2"); + EXPECT_FALSE(adapter->HasHeader("X-Request-2")); + + adapter->SetExtraHeaderByName("X-Request-3", "Baz"); + EXPECT_TRUE(adapter->HasHeader("X-Request-3")); + + EXPECT_EQ(kTestRedirectURL, redirect_url); + + adapter->SetDestructionCallback(ignored_destruction_callback.Get()); + })); + + net::RedirectInfo redirect_info; + redirect_info.new_url = kTestRedirectURL; + // An HTTPS to HTTPS redirect such as this wouldn't normally change the + // referrer but we do for testing purposes. + redirect_info.new_referrer = kTestURL.spec(); + + auto response_head = network::mojom::URLResponseHead::New(); + response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(""); + response_head->headers->SetHeader("X-Response-1", "Foo"); + response_head->headers->SetHeader("X-Response-2", "Bar"); + + std::vector<std::string> request_headers_to_remove; + net::HttpRequestHeaders modified_request_headers; + net::HttpRequestHeaders modified_cors_exempt_request_headers; + throttle->WillRedirectRequest( + &redirect_info, *response_head, &defer, &request_headers_to_remove, + &modified_request_headers, &modified_cors_exempt_request_headers); + + EXPECT_FALSE(defer); + + EXPECT_TRUE(response_head->headers->HasHeader("X-Response-1")); + EXPECT_FALSE(response_head->headers->HasHeader("X-Response-2")); + + EXPECT_THAT(request_headers_to_remove, ElementsAre("X-Request-2")); + EXPECT_TRUE(modified_request_headers.GetHeader("X-Request-3", &value)); + EXPECT_EQ("Baz", value); + + EXPECT_TRUE(modified_cors_exempt_request_headers.IsEmpty()); + + testing::Mock::VerifyAndClearExpectations(delegate); + + // Phase 3: Complete the request. + + EXPECT_CALL(*delegate, ProcessResponse(_, _)) + .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin()); + EXPECT_TRUE(adapter->IsMainFrame()); + + EXPECT_EQ(response_user_data_ptr, + adapter->GetUserData(kResponseUserDataKey)); + + const net::HttpResponseHeaders* headers = adapter->GetHeaders(); + // This is a new response and so previous headers should not carry over. + EXPECT_FALSE(headers->HasHeader("X-Response-1")); + EXPECT_FALSE(headers->HasHeader("X-Response-2")); + + EXPECT_TRUE(headers->HasHeader("X-Response-3")); + EXPECT_TRUE(headers->HasHeader("X-Response-4")); + adapter->RemoveHeader("X-Response-3"); + + EXPECT_EQ(GURL(), redirect_url); + })); + + response_head = network::mojom::URLResponseHead::New(); + response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(""); + response_head->headers->SetHeader("X-Response-3", "Foo"); + response_head->headers->SetHeader("X-Response-4", "Bar"); + + throttle->WillProcessResponse(kTestRedirectURL, response_head.get(), &defer); + + EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3")); + EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4")); + + EXPECT_FALSE(defer); + + EXPECT_CALL(destruction_callback, Run()).Times(1); + EXPECT_CALL(ignored_destruction_callback, Run()).Times(0); + throttle.reset(); +} + +TEST(ChromeSigninURLLoaderThrottleTest, InterceptSubFrame) { + auto* delegate = new MockDelegate(); + EXPECT_CALL(*delegate, ShouldInterceptNavigation(_)).WillOnce(Return(true)); + auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate), + NullWebContentsGetter()); + ASSERT_TRUE(throttle); + + EXPECT_CALL(*delegate, ProcessRequest(_, _)) + .Times(2) + .WillRepeatedly( + [](ChromeRequestAdapter* adapter, const GURL& redirect_url) { + EXPECT_EQ(network::mojom::RequestDestination::kIframe, + adapter->GetRequestDestination()); + }); + + network::ResourceRequest request; + request.url = GURL("https://google.com"); + request.destination = network::mojom::RequestDestination::kIframe; + + bool defer = false; + throttle->WillStartRequest(&request, &defer); + EXPECT_FALSE(defer); + + EXPECT_CALL(*delegate, ProcessResponse(_, _)) + .Times(2) + .WillRepeatedly(([](ResponseAdapter* adapter, const GURL& redirect_url) { + EXPECT_FALSE(adapter->IsMainFrame()); + })); + + net::RedirectInfo redirect_info; + redirect_info.new_url = GURL("https://youtube.com"); + auto response_head = network::mojom::URLResponseHead::New(); + + std::vector<std::string> request_headers_to_remove; + net::HttpRequestHeaders modified_request_headers; + net::HttpRequestHeaders modified_cors_exempt_request_headers; + throttle->WillRedirectRequest( + &redirect_info, *response_head, &defer, &request_headers_to_remove, + &modified_request_headers, &modified_cors_exempt_request_headers); + EXPECT_FALSE(defer); + EXPECT_TRUE(request_headers_to_remove.empty()); + EXPECT_TRUE(modified_request_headers.IsEmpty()); + EXPECT_TRUE(modified_cors_exempt_request_headers.IsEmpty()); + + throttle->WillProcessResponse(GURL("https://youtube.com"), + response_head.get(), &defer); + EXPECT_FALSE(defer); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc b/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc new file mode 100644 index 00000000000..b5b9b6a5379 --- /dev/null +++ b/chromium/chrome/browser/signin/chromeos_mirror_account_consistency_browsertest.cc @@ -0,0 +1,174 @@ +// Copyright 2017 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. + +#include "chrome/browser/ash/login/login_manager_test.h" +#include "chrome/browser/ash/login/test/login_manager_mixin.h" +#include "chrome/browser/ash/profiles/profile_helper.h" +#include "chrome/browser/prefs/incognito_mode_prefs.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_key.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/supervised_user/supervised_user_constants.h" +#include "chrome/browser/supervised_user/supervised_user_settings_service.h" +#include "chrome/browser/supervised_user/supervised_user_settings_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/account_id/account_id.h" +#include "components/google/core/common/google_switches.h" +#include "components/network_session_configurator/common/network_switches.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/user_manager/user.h" +#include "components/user_manager/user_manager.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "net/test/embedded_test_server/default_handlers.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { + +constexpr char kGaiaDomain[] = "accounts.google.com"; + +// Checks whether the "X-Chrome-Connected" header of a new request to Google +// contains |expected_header_value|. +void TestMirrorRequestForProfile(net::EmbeddedTestServer* test_server, + Profile* profile, + const std::string& expected_header_value) { + GURL gaia_url(test_server->GetURL("/echoheader?X-Chrome-Connected")); + GURL::Replacements replace_host; + replace_host.SetHostStr(kGaiaDomain); + gaia_url = gaia_url.ReplaceComponents(replace_host); + + Browser* browser = Browser::Create(Browser::CreateParams(profile, true)); + ui_test_utils::NavigateToURLWithDisposition( + browser, gaia_url, WindowOpenDisposition::SINGLETON_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + + std::string inner_text; + ASSERT_TRUE(content::ExecuteScriptAndExtractString( + browser->tab_strip_model()->GetActiveWebContents(), + "domAutomationController.send(document.body.innerText);", &inner_text)); + // /echoheader returns "None" if the header isn't set. + inner_text = (inner_text == "None") ? "" : inner_text; + EXPECT_EQ(expected_header_value, inner_text); +} + +} // namespace + +// This is a Chrome OS-only test ensuring that mirror account consistency is +// enabled for child accounts, but not enabled for other account types. +class ChromeOsMirrorAccountConsistencyTest : public ash::LoginManagerTest { + public: + ChromeOsMirrorAccountConsistencyTest( + const ChromeOsMirrorAccountConsistencyTest&) = delete; + ChromeOsMirrorAccountConsistencyTest& operator=( + const ChromeOsMirrorAccountConsistencyTest&) = delete; + + protected: + ~ChromeOsMirrorAccountConsistencyTest() override {} + + ChromeOsMirrorAccountConsistencyTest() : LoginManagerTest() { + login_mixin_.AppendRegularUsers(1); + account_id_ = login_mixin_.users()[0].account_id; + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + ash::LoginManagerTest::SetUpCommandLine(command_line); + + // HTTPS server only serves a valid cert for localhost, so this is needed to + // load pages from "www.google.com" without an interstitial. + command_line->AppendSwitch(switches::kIgnoreCertificateErrors); + + // The production code only allows known ports (80 for http and 443 for + // https), but the test server runs on a random port. + command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers); + } + + void SetUpOnMainThread() override { + // We can't use BrowserTestBase's EmbeddedTestServer because google.com + // URL's have to be https. + test_server_ = std::make_unique<net::EmbeddedTestServer>( + net::EmbeddedTestServer::TYPE_HTTPS); + net::test_server::RegisterDefaultHandlers(test_server_.get()); + ASSERT_TRUE(test_server_->Start()); + + ash::LoginManagerTest::SetUpOnMainThread(); + } + + AccountId account_id_; + ash::LoginManagerMixin login_mixin_{&mixin_host_}; + + protected: + std::unique_ptr<net::EmbeddedTestServer> test_server_; +}; + +// Mirror is enabled for child accounts. +IN_PROC_BROWSER_TEST_F(ChromeOsMirrorAccountConsistencyTest, + TestMirrorRequestChromeOsChildAccount) { + // Child user. + LoginUser(account_id_); + + user_manager::User* user = user_manager::UserManager::Get()->GetActiveUser(); + ASSERT_EQ(user, user_manager::UserManager::Get()->GetPrimaryUser()); + ASSERT_EQ(user, user_manager::UserManager::Get()->FindUser(account_id_)); + Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(user); + + // Supervised flag uses `FindExtendedAccountInfoForAccountWithRefreshToken`, + // so wait for tokens to be loaded. + signin::WaitForRefreshTokensLoaded( + IdentityManagerFactory::GetForProfile(profile)); + + SupervisedUserSettingsService* supervised_user_settings_service = + SupervisedUserSettingsServiceFactory::GetForKey(profile->GetProfileKey()); + supervised_user_settings_service->SetActive(true); + + // Incognito is always disabled for child accounts. + PrefService* prefs = profile->GetPrefs(); + prefs->SetInteger( + prefs::kIncognitoModeAvailability, + static_cast<int>(IncognitoModePrefs::Availability::kDisabled)); + ASSERT_EQ(1, signin::PROFILE_MODE_INCOGNITO_DISABLED); + + // TODO(http://crbug.com/1134144): This test seems to test supervised profiles + // instead of child accounts. With the current implementation, + // X-Chrome-Connected header gets a supervised=true argument only for child + // profiles. Verify if these tests needs to be updated to use child accounts + // or whether supervised profiles need to be supported as well. + TestMirrorRequestForProfile( + test_server_.get(), profile, + "source=Chrome,mode=1,enable_account_consistency=true,supervised=false," + "consistency_enabled_by_default=false"); +} + +// Mirror is enabled for non-child accounts. +IN_PROC_BROWSER_TEST_F(ChromeOsMirrorAccountConsistencyTest, + TestMirrorRequestChromeOsNotChildAccount) { + // Not a child user. + LoginUser(account_id_); + + user_manager::User* user = user_manager::UserManager::Get()->GetActiveUser(); + ASSERT_EQ(user, user_manager::UserManager::Get()->GetPrimaryUser()); + ASSERT_EQ(user, user_manager::UserManager::Get()->FindUser(account_id_)); + Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(user); + + // Supervised flag uses `FindExtendedAccountInfoForAccountWithRefreshToken`, + // so wait for tokens to be loaded. + signin::WaitForRefreshTokensLoaded( + IdentityManagerFactory::GetForProfile(profile)); + + // With Chrome OS Account Manager enabled, this should be true. + EXPECT_TRUE( + AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile)); + TestMirrorRequestForProfile( + test_server_.get(), profile, + "source=Chrome,mode=0,enable_account_consistency=true,supervised=false," + "consistency_enabled_by_default=false"); +} diff --git a/chromium/chrome/browser/signin/cookie_reminter_factory.cc b/chromium/chrome/browser/signin/cookie_reminter_factory.cc new file mode 100644 index 00000000000..f53de57d249 --- /dev/null +++ b/chromium/chrome/browser/signin/cookie_reminter_factory.cc @@ -0,0 +1,37 @@ +// 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. + +#include "chrome/browser/signin/cookie_reminter_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/signin/core/browser/cookie_reminter.h" + +CookieReminterFactory::CookieReminterFactory() + : BrowserContextKeyedServiceFactory( + "CookieReminter", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +CookieReminterFactory::~CookieReminterFactory() {} + +// static +CookieReminter* CookieReminterFactory::GetForProfile(Profile* profile) { + return static_cast<CookieReminter*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +CookieReminterFactory* CookieReminterFactory::GetInstance() { + return base::Singleton<CookieReminterFactory>::get(); +} + +KeyedService* CookieReminterFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + return new CookieReminter(identity_manager); +} diff --git a/chromium/chrome/browser/signin/cookie_reminter_factory.h b/chromium/chrome/browser/signin/cookie_reminter_factory.h new file mode 100644 index 00000000000..8ce4768bebd --- /dev/null +++ b/chromium/chrome/browser/signin/cookie_reminter_factory.h @@ -0,0 +1,30 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class CookieReminter; +class Profile; + +class CookieReminterFactory : public BrowserContextKeyedServiceFactory { + public: + static CookieReminter* GetForProfile(Profile* profile); + static CookieReminterFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits<CookieReminterFactory>; + + CookieReminterFactory(); + ~CookieReminterFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_COOKIE_REMINTER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/dice_browsertest.cc b/chromium/chrome/browser/signin/dice_browsertest.cc new file mode 100644 index 00000000000..150647b8b0b --- /dev/null +++ b/chromium/chrome/browser/signin/dice_browsertest.cc @@ -0,0 +1,1236 @@ +// Copyright 2017 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. + +#include <map> +#include <memory> +#include <string> +#include <utility> + +#include "base/auto_reset.h" +#include "base/base_switches.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/check.h" +#include "base/command_line.h" +#include "base/location.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/test_mock_time_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "chrome/browser/apps/platform_apps/shortcut_manager.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/extensions/api/identity/web_auth_flow.h" +#include "chrome/browser/policy/cloud/user_policy_signin_service.h" +#include "chrome/browser/policy/cloud/user_policy_signin_service_internal.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/chrome_device_id_helper.h" +#include "chrome/browser/signin/chrome_signin_client.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/dice_response_handler.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/sync/user_event_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/profile_chooser_constants.h" +#include "chrome/browser/ui/simple_message_box_internal.h" +#include "chrome/browser/ui/webui/signin/login_ui_service.h" +#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h" +#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/url_constants.h" +#include "chrome/common/webui_url_constants.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/prefs/pref_service.h" +#include "components/search/ntp_features.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/dice_header_helper.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/base/signin_client.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "components/sync/base/sync_prefs.h" +#include "components/sync_user_events/user_event_service.h" +#include "components/variations/variations_switches.h" +#include "content/public/browser/browser_task_traits.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/load_notification_details.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/gaia_switches.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/test/embedded_test_server/request_handler_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using net::test_server::BasicHttpResponse; +using net::test_server::HttpRequest; +using net::test_server::HttpResponse; +using signin::AccountConsistencyMethod; + +namespace { + +constexpr int kAccountReconcilorDelayMs = 10; + +enum SignoutType { + kSignoutTypeFirst = 0, + + kAllAccounts = 0, // Sign out from all accounts. + kMainAccount = 1, // Sign out from main account only. + kSecondaryAccount = 2, // Sign out from secondary account only. + + kSignoutTypeLast +}; + +const char kAuthorizationCode[] = "authorization_code"; +const char kDiceResponseHeader[] = "X-Chrome-ID-Consistency-Response"; +const char kChromeSyncEndpointURL[] = "/signin/chrome/sync"; +const char kEnableSyncURL[] = "/enable_sync"; +const char kGoogleSignoutResponseHeader[] = "Google-Accounts-SignOut"; +const char kMainGmailEmail[] = "main_email@gmail.com"; +const char kMainManagedEmail[] = "main_email@managed.com"; +const char kNoDiceRequestHeader[] = "NoDiceHeader"; +const char kOAuth2TokenExchangeURL[] = "/oauth2/v4/token"; +const char kOAuth2TokenRevokeURL[] = "/o/oauth2/revoke"; +const char kSecondaryEmail[] = "secondary_email@example.com"; +const char kSigninURL[] = "/signin"; +const char kSigninWithOutageInDiceURL[] = "/signin/outage"; +const char kSignoutURL[] = "/signout"; + +// Test response that does not complete synchronously. It must be unblocked by +// calling the completion closure. +class BlockedHttpResponse : public net::test_server::BasicHttpResponse { + public: + explicit BlockedHttpResponse( + base::OnceCallback<void(base::OnceClosure)> callback) + : callback_(std::move(callback)) {} + + void SendResponse( + base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override { + // Called on the IO thread to unblock the response. + base::OnceClosure unblock_io_thread = + base::BindOnce(&BlockedHttpResponse::SendResponseInternal, + weak_factory_.GetWeakPtr(), delegate); + // Unblock the response from any thread by posting a task to the IO thread. + base::OnceClosure unblock_any_thread = + base::BindOnce(base::IgnoreResult(&base::TaskRunner::PostTask), + base::ThreadTaskRunnerHandle::Get(), FROM_HERE, + std::move(unblock_io_thread)); + // Pass |unblock_any_thread| to the caller on the UI thread. + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(std::move(callback_), std::move(unblock_any_thread))); + } + + private: + void SendResponseInternal( + base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) { + if (delegate) + BasicHttpResponse::SendResponse(delegate); + } + base::OnceCallback<void(base::OnceClosure)> callback_; + + base::WeakPtrFactory<BlockedHttpResponse> weak_factory_{this}; +}; + +} // namespace + +namespace FakeGaia { + +// Handler for the signin page on the embedded test server. +// The response has the content of the Dice request header in its body, and has +// the Dice response header. +// Handles both the "Chrome Sync" endpoint and the old endpoint. +std::unique_ptr<HttpResponse> HandleSigninURL( + const std::string& main_email, + const base::RepeatingCallback<void(const std::string&)>& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kSigninURL) && + !net::test_server::ShouldHandle(request, kChromeSyncEndpointURL) && + !net::test_server::ShouldHandle(request, kSigninWithOutageInDiceURL)) + return nullptr; + + // Extract Dice request header. + std::string header_value = kNoDiceRequestHeader; + auto it = request.headers.find(signin::kDiceRequestHeader); + if (it != request.headers.end()) + header_value = it->second; + + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(callback, header_value)); + + // Add the SIGNIN dice header. + std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse); + if (header_value != kNoDiceRequestHeader) { + if (net::test_server::ShouldHandle(request, kSigninWithOutageInDiceURL)) { + http_response->AddCustomHeader( + kDiceResponseHeader, + base::StringPrintf("action=SIGNIN,authuser=1,id=%s,email=%s," + "no_authorization_code=true", + signin::GetTestGaiaIdForEmail(main_email).c_str(), + main_email.c_str())); + } else { + http_response->AddCustomHeader( + kDiceResponseHeader, + base::StringPrintf( + "action=SIGNIN,authuser=1,id=%s,email=%s,authorization_code=%s", + signin::GetTestGaiaIdForEmail(main_email).c_str(), + main_email.c_str(), kAuthorizationCode)); + } + } + + // When hitting the Chrome Sync endpoint, redirect to kEnableSyncURL, which + // adds the ENABLE_SYNC dice header. + if (net::test_server::ShouldHandle(request, kChromeSyncEndpointURL)) { + http_response->set_code(net::HTTP_FOUND); // 302 redirect. + http_response->AddCustomHeader("location", kEnableSyncURL); + } + + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for the Gaia endpoint adding the ENABLE_SYNC dice header. +std::unique_ptr<HttpResponse> HandleEnableSyncURL( + const std::string& main_email, + const base::RepeatingCallback<void(base::OnceClosure)>& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kEnableSyncURL)) + return nullptr; + + std::unique_ptr<BlockedHttpResponse> http_response = + std::make_unique<BlockedHttpResponse>(callback); + http_response->AddCustomHeader( + kDiceResponseHeader, + base::StringPrintf("action=ENABLE_SYNC,authuser=1,id=%s,email=%s", + signin::GetTestGaiaIdForEmail(main_email).c_str(), + main_email.c_str())); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for the signout page on the embedded test server. +// Responds with a Google-Accounts-SignOut header for the main account, the +// secondary account, or both (depending on the SignoutType, which is encoded in +// the query string). +std::unique_ptr<HttpResponse> HandleSignoutURL(const std::string& main_email, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kSignoutURL)) + return nullptr; + + // Build signout header. + int query_value; + EXPECT_TRUE(base::StringToInt(request.GetURL().query(), &query_value)); + SignoutType signout_type = static_cast<SignoutType>(query_value); + EXPECT_GE(signout_type, kSignoutTypeFirst); + EXPECT_LT(signout_type, kSignoutTypeLast); + std::string signout_header_value; + if (signout_type == kAllAccounts || signout_type == kMainAccount) { + std::string main_gaia_id = signin::GetTestGaiaIdForEmail(main_email); + signout_header_value = + base::StringPrintf("email=\"%s\", obfuscatedid=\"%s\", sessionindex=1", + main_email.c_str(), main_gaia_id.c_str()); + } + if (signout_type == kAllAccounts || signout_type == kSecondaryAccount) { + if (!signout_header_value.empty()) + signout_header_value += ", "; + std::string secondary_gaia_id = + signin::GetTestGaiaIdForEmail(kSecondaryEmail); + signout_header_value += + base::StringPrintf("email=\"%s\", obfuscatedid=\"%s\", sessionindex=2", + kSecondaryEmail, secondary_gaia_id.c_str()); + } + + std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse); + http_response->AddCustomHeader(kGoogleSignoutResponseHeader, + signout_header_value); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for OAuth2 token exchange. +// Checks that the request is well formatted and returns a refresh token in a +// JSON dictionary. +std::unique_ptr<HttpResponse> HandleOAuth2TokenExchangeURL( + const base::RepeatingCallback<void(base::OnceClosure)>& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kOAuth2TokenExchangeURL)) + return nullptr; + + // Check that the authorization code is somewhere in the request body. + if (!request.has_content) + return nullptr; + if (request.content.find(kAuthorizationCode) == std::string::npos) + return nullptr; + + std::unique_ptr<BlockedHttpResponse> http_response = + std::make_unique<BlockedHttpResponse>(callback); + + std::string content = + "{" + " \"access_token\":\"access_token\"," + " \"refresh_token\":\"new_refresh_token\"," + " \"expires_in\":9999" + "}"; + + http_response->set_content(content); + http_response->set_content_type("text/plain"); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for OAuth2 token revocation. +std::unique_ptr<HttpResponse> HandleOAuth2TokenRevokeURL( + const base::RepeatingClosure& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, kOAuth2TokenRevokeURL)) + return nullptr; + + content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, callback); + + std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +// Handler for ServiceLogin on the embedded test server. +// Calls the callback with the dice request header, or kNoDiceRequestHeader if +// there is no Dice header. +std::unique_ptr<HttpResponse> HandleChromeSigninEmbeddedURL( + const base::RepeatingCallback<void(const std::string&)>& callback, + const HttpRequest& request) { + if (!net::test_server::ShouldHandle(request, + "/embedded/setup/chrome/usermenu")) + return nullptr; + + std::string dice_request_header(kNoDiceRequestHeader); + auto it = request.headers.find(signin::kDiceRequestHeader); + if (it != request.headers.end()) + dice_request_header = it->second; + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(callback, dice_request_header)); + + std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse); + http_response->AddCustomHeader("Cache-Control", "no-store"); + return std::move(http_response); +} + +} // namespace FakeGaia + +class DiceBrowserTest : public InProcessBrowserTest, + public AccountReconcilor::Observer, + public signin::IdentityManager::Observer { + public: + DiceBrowserTest(const DiceBrowserTest&) = delete; + DiceBrowserTest& operator=(const DiceBrowserTest&) = delete; + + protected: + ~DiceBrowserTest() override {} + + explicit DiceBrowserTest(const std::string& main_email = kMainGmailEmail) + : main_email_(main_email), + https_server_(net::EmbeddedTestServer::TYPE_HTTPS), + enable_sync_requested_(false), + token_requested_(false), + refresh_token_available_(false), + token_revoked_notification_count_(0), + token_revoked_count_(0), + reconcilor_blocked_count_(0), + reconcilor_unblocked_count_(0), + reconcilor_started_count_(0) { + feature_list_.InitAndEnableFeature(kSupportOAuthOutageInDice); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleSigninURL, main_email_, + base::BindRepeating(&DiceBrowserTest::OnSigninRequest, + base::Unretained(this)))); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleEnableSyncURL, main_email_, + base::BindRepeating(&DiceBrowserTest::OnEnableSyncRequest, + base::Unretained(this)))); + https_server_.RegisterDefaultHandler( + base::BindRepeating(&FakeGaia::HandleSignoutURL, main_email_)); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleOAuth2TokenExchangeURL, + base::BindRepeating(&DiceBrowserTest::OnTokenExchangeRequest, + base::Unretained(this)))); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleOAuth2TokenRevokeURL, + base::BindRepeating(&DiceBrowserTest::OnTokenRevocationRequest, + base::Unretained(this)))); + https_server_.RegisterDefaultHandler(base::BindRepeating( + &FakeGaia::HandleChromeSigninEmbeddedURL, + base::BindRepeating(&DiceBrowserTest::OnChromeSigninEmbeddedRequest, + base::Unretained(this)))); + signin::SetDiceAccountReconcilorBlockDelayForTesting( + kAccountReconcilorDelayMs); + } + + // Navigates to the given path on the test server. + void NavigateToURL(const std::string& path) { + ASSERT_TRUE( + ui_test_utils::NavigateToURL(browser(), https_server_.GetURL(path))); + } + + // Returns the identity manager. + signin::IdentityManager* GetIdentityManager() { + return IdentityManagerFactory::GetForProfile(browser()->profile()); + } + + // Returns the account ID associated with |main_email_| and its associated + // gaia ID. + CoreAccountId GetMainAccountID() { + return GetIdentityManager()->PickAccountIdForAccount( + signin::GetTestGaiaIdForEmail(main_email_), main_email_); + } + + // Returns the account ID associated with kSecondaryEmail and its associated + // gaia ID. + CoreAccountId GetSecondaryAccountID() { + return GetIdentityManager()->PickAccountIdForAccount( + signin::GetTestGaiaIdForEmail(kSecondaryEmail), kSecondaryEmail); + } + + std::string GetDeviceId() { + return GetSigninScopedDeviceIdForProfile(browser()->profile()); + } + + // Signin with a main account and add token for a secondary account. + void SetupSignedInAccounts( + signin::ConsentLevel primary_account_consent_level) { + // Signin main account. + AccountInfo primary_account_info = signin::MakePrimaryAccountAvailable( + GetIdentityManager(), main_email_, primary_account_consent_level); + ASSERT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + ASSERT_FALSE( + GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + GetMainAccountID())); + ASSERT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + primary_account_consent_level)); + + // Add a token for a secondary account. + AccountInfo secondary_account_info = + signin::MakeAccountAvailable(GetIdentityManager(), kSecondaryEmail); + ASSERT_TRUE(GetIdentityManager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + ASSERT_FALSE( + GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + secondary_account_info.account_id)); + } + + // Navigate to a Gaia URL setting the Google-Accounts-SignOut header. + void SignOutWithDice(SignoutType signout_type) { + NavigateToURL(base::StringPrintf("%s?%i", kSignoutURL, signout_type)); + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + + base::RunLoop().RunUntilIdle(); + } + + // InProcessBrowserTest: + void SetUp() override { + ASSERT_TRUE(https_server_.InitializeAndListen()); + InProcessBrowserTest::SetUp(); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + const GURL& base_url = https_server_.base_url(); + command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec()); + command_line->AppendSwitchASCII(switches::kGoogleApisUrl, base_url.spec()); + command_line->AppendSwitchASCII(switches::kLsoUrl, base_url.spec()); + } + + void SetUpOnMainThread() override { + InProcessBrowserTest::SetUpOnMainThread(); + https_server_.StartAcceptingConnections(); + + GetIdentityManager()->AddObserver(this); + // Wait for the token service to be ready. + if (!GetIdentityManager()->AreRefreshTokensLoaded()) { + WaitForClosure(&tokens_loaded_quit_closure_); + } + ASSERT_TRUE(GetIdentityManager()->AreRefreshTokensLoaded()); + + AccountReconcilor* reconcilor = + AccountReconcilorFactory::GetForProfile(browser()->profile()); + + // Reconcilor starts as soon as the token service finishes loading its + // credentials. Abort the reconcilor here to make sure tests start in a + // stable state. + reconcilor->AbortReconcile(); + reconcilor->SetState( + signin_metrics::AccountReconcilorState::ACCOUNT_RECONCILOR_OK); + reconcilor->AddObserver(this); + } + + void TearDownOnMainThread() override { + GetIdentityManager()->RemoveObserver(this); + AccountReconcilorFactory::GetForProfile(browser()->profile()) + ->RemoveObserver(this); + } + + // Calls |closure| if it is not null and resets it after. + void RunClosureIfValid(base::OnceClosure closure) { + if (closure) + std::move(closure).Run(); + } + + // Creates and runs a RunLoop until |closure| is called. + void WaitForClosure(base::OnceClosure* closure) { + base::RunLoop run_loop; + *closure = run_loop.QuitClosure(); + run_loop.Run(); + } + + // FakeGaia callbacks: + void OnSigninRequest(const std::string& dice_request_header) { + EXPECT_EQ(dice_request_header != kNoDiceRequestHeader, + IsReconcilorBlocked()); + dice_request_header_ = dice_request_header; + RunClosureIfValid(std::move(signin_requested_quit_closure_)); + } + + void OnChromeSigninEmbeddedRequest(const std::string& dice_request_header) { + dice_request_header_ = dice_request_header; + RunClosureIfValid(std::move(chrome_signin_embedded_quit_closure_)); + } + + void OnEnableSyncRequest(base::OnceClosure unblock_response_closure) { + EXPECT_TRUE(IsReconcilorBlocked()); + enable_sync_requested_ = true; + RunClosureIfValid(std::move(enable_sync_requested_quit_closure_)); + unblock_enable_sync_response_closure_ = std::move(unblock_response_closure); + } + + void OnTokenExchangeRequest(base::OnceClosure unblock_response_closure) { + // The token must be exchanged only once. + EXPECT_FALSE(token_requested_); + EXPECT_TRUE(IsReconcilorBlocked()); + token_requested_ = true; + RunClosureIfValid(std::move(token_requested_quit_closure_)); + unblock_token_exchange_response_closure_ = + std::move(unblock_response_closure); + } + + void OnTokenRevocationRequest() { + ++token_revoked_count_; + RunClosureIfValid(std::move(token_revoked_quit_closure_)); + } + + // AccountReconcilor::Observer: + void OnBlockReconcile() override { ++reconcilor_blocked_count_; } + void OnUnblockReconcile() override { + ++reconcilor_unblocked_count_; + RunClosureIfValid(std::move(unblock_count_quit_closure_)); + } + void OnStateChanged(signin_metrics::AccountReconcilorState state) override { + if (state == + signin_metrics::AccountReconcilorState::ACCOUNT_RECONCILOR_RUNNING) { + ++reconcilor_started_count_; + } + } + + // signin::IdentityManager::Observer + void OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) override { + if (event.GetEventTypeFor(signin::ConsentLevel::kSync) == + signin::PrimaryAccountChangeEvent::Type::kSet) { + RunClosureIfValid(std::move(on_primary_account_set_quit_closure_)); + } + } + + void OnRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info) override { + if (account_info.account_id == GetMainAccountID()) { + refresh_token_available_ = true; + RunClosureIfValid(std::move(refresh_token_available_quit_closure_)); + } + } + + void OnRefreshTokenRemovedForAccount( + const CoreAccountId& account_id) override { + ++token_revoked_notification_count_; + } + + void OnRefreshTokensLoaded() override { + RunClosureIfValid(std::move(tokens_loaded_quit_closure_)); + } + + // Returns true if the account reconcilor is currently blocked. + bool IsReconcilorBlocked() { + EXPECT_GE(reconcilor_blocked_count_, reconcilor_unblocked_count_); + EXPECT_LE(reconcilor_blocked_count_, reconcilor_unblocked_count_ + 1); + return (reconcilor_unblocked_count_ + 1) == reconcilor_blocked_count_; + } + + // Waits until |reconcilor_unblocked_count_| reaches |count|. + void WaitForReconcilorUnblockedCount(int count) { + if (reconcilor_unblocked_count_ == count) + return; + + ASSERT_EQ(count - 1, reconcilor_unblocked_count_); + // Wait for the timeout after the request is complete. + WaitForClosure(&unblock_count_quit_closure_); + EXPECT_EQ(count, reconcilor_unblocked_count_); + } + + // Waits until the user consented for sync. + void WaitForSigninSucceeded() { + if (GetIdentityManager() + ->GetPrimaryAccountId(signin::ConsentLevel::kSync) + .empty()) { + WaitForClosure(&on_primary_account_set_quit_closure_); + } + } + + // Waits for the ENABLE_SYNC request to hit the server, and unblocks the + // response. If this is not called, ENABLE_SYNC will not be sent by the + // server. + // Note: this does not wait for the response to reach Chrome. + void SendEnableSyncResponse() { + if (!enable_sync_requested_) + WaitForClosure(&enable_sync_requested_quit_closure_); + DCHECK(unblock_enable_sync_response_closure_); + std::move(unblock_enable_sync_response_closure_).Run(); + } + + // Waits until the token request is sent to the server, the response is + // received and the refresh token is available. If this is not called, the + // refresh token will not be sent by the server. + void SendRefreshTokenResponse() { + // Wait for the request hitting the server. + if (!token_requested_) + WaitForClosure(&token_requested_quit_closure_); + EXPECT_TRUE(token_requested_); + // Unblock the server response. + DCHECK(unblock_token_exchange_response_closure_); + std::move(unblock_token_exchange_response_closure_).Run(); + // Wait for the response coming back. + if (!refresh_token_available_) + WaitForClosure(&refresh_token_available_quit_closure_); + EXPECT_TRUE(refresh_token_available_); + } + + void WaitForTokenRevokedCount(int count) { + EXPECT_LE(token_revoked_count_, count); + while (token_revoked_count_ < count) + WaitForClosure(&token_revoked_quit_closure_); + EXPECT_EQ(count, token_revoked_count_); + } + + DiceResponseHandler* GetDiceResponseHandler() { + return DiceResponseHandler::GetForProfile(browser()->profile()); + } + + const std::string main_email_; + net::EmbeddedTestServer https_server_; + bool enable_sync_requested_; + bool token_requested_; + bool refresh_token_available_; + int token_revoked_notification_count_; + int token_revoked_count_; + int reconcilor_blocked_count_; + int reconcilor_unblocked_count_; + int reconcilor_started_count_; + std::string dice_request_header_; + base::test::ScopedFeatureList feature_list_; + + // Unblocks the server responses. + base::OnceClosure unblock_token_exchange_response_closure_; + base::OnceClosure unblock_enable_sync_response_closure_; + + // Used for waiting asynchronous events. + base::OnceClosure enable_sync_requested_quit_closure_; + base::OnceClosure token_requested_quit_closure_; + base::OnceClosure token_revoked_quit_closure_; + base::OnceClosure refresh_token_available_quit_closure_; + base::OnceClosure chrome_signin_embedded_quit_closure_; + base::OnceClosure unblock_count_quit_closure_; + base::OnceClosure tokens_loaded_quit_closure_; + base::OnceClosure on_primary_account_set_quit_closure_; + base::OnceClosure signin_requested_quit_closure_; +}; + +// Checks that signin on Gaia triggers the fetch for a refresh token. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Signin) { + EXPECT_EQ(0, reconcilor_started_count_); + + // Navigate to Gaia and sign in. + NavigateToURL(kSigninURL); + + // Check that the Dice request header was sent. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + // Check that the token was requested and added to the token service. + SendRefreshTokenResponse(); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + // Sync should not be enabled. + EXPECT_TRUE(GetIdentityManager() + ->GetPrimaryAccountId(signin::ConsentLevel::kSync) + .empty()); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(1, reconcilor_started_count_); +} + +// Checks that the account reconcilor is blocked when where was OAuth +// outage in Dice, and unblocked after the timeout. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SupportOAuthOutageInDice) { + DiceResponseHandler* dice_response_handler = GetDiceResponseHandler(); + scoped_refptr<base::TestMockTimeTaskRunner> task_runner = + new base::TestMockTimeTaskRunner(); + dice_response_handler->SetTaskRunner(task_runner); + NavigateToURL(kSigninWithOutageInDiceURL); + // Check that the Dice request header was sent. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + // Check that the reconcilor was blocked and not unblocked before timeout. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + task_runner->FastForwardBy( + base::Hours(kLockAccountReconcilorTimeoutHours / 2)); + EXPECT_EQ(0, reconcilor_unblocked_count_); + task_runner->FastForwardBy( + base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2)); + // Wait until reconcilor is unblocked. + WaitForReconcilorUnblockedCount(1); +} + +// Checks that re-auth on Gaia triggers the fetch for a refresh token. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Reauth) { + EXPECT_EQ(0, reconcilor_started_count_); + + // Start from a signed-in state. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + EXPECT_EQ(1, reconcilor_started_count_); + + // Navigate to Gaia and sign in again with the main account. + NavigateToURL(kSigninURL); + + // Check that the Dice request header was sent. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + // Check that the token was requested and added to the token service. + SendRefreshTokenResponse(); + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + + // Old token must not be revoked (see http://crbug.com/865189). + EXPECT_EQ(0, token_revoked_notification_count_); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(2, reconcilor_started_count_); +} + +// Checks that the Dice signout flow works and deletes all tokens. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutMainAccount) { + // Start from a signed-in state. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + + // Signout from main account. + SignOutWithDice(kMainAccount); + + // Check that the user is in error state. + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + GetMainAccountID())); + EXPECT_TRUE(GetIdentityManager()->HasAccountWithRefreshToken( + GetSecondaryAccountID())); + + // Token for main account is revoked on server but not notified in the client. + EXPECT_EQ(0, token_revoked_notification_count_); + WaitForTokenRevokedCount(1); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); +} + +// Checks that signing out from a secondary account does not delete the main +// token. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutSecondaryAccount) { + // Start from a signed-in state. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + + // Signout from secondary account. + SignOutWithDice(kSecondaryAccount); + + // Check that the user is still signed in from main account, but secondary + // token is deleted. + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_FALSE(GetIdentityManager()->HasAccountWithRefreshToken( + GetSecondaryAccountID())); + EXPECT_EQ(1, token_revoked_notification_count_); + WaitForTokenRevokedCount(1); + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); +} + +// Checks that the Dice signout flow works and deletes all tokens. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, SignoutAllAccounts) { + // Start from a signed-in state. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + + // Signout from all accounts. + SignOutWithDice(kAllAccounts); + + // Check that the user is in error state. + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshTokenInPersistentErrorState( + GetMainAccountID())); + EXPECT_FALSE(GetIdentityManager()->HasAccountWithRefreshToken( + GetSecondaryAccountID())); + + // Token for main account is revoked on server but not notified in the client. + EXPECT_EQ(1, token_revoked_notification_count_); + WaitForTokenRevokedCount(2); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); +} + +// Checks that Dice request header is not set from request from WebUI. +// See https://crbug.com/428396 +#if defined(OS_WIN) +#define MAYBE_NoDiceFromWebUI DISABLED_NoDiceFromWebUI +#else +#define MAYBE_NoDiceFromWebUI NoDiceFromWebUI +#endif +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, MAYBE_NoDiceFromWebUI) { + // Navigate to Gaia and from the native tab, which uses an extension. + ASSERT_TRUE(ui_test_utils::NavigateToURL( + browser(), GURL("chrome:chrome-signin?reason=5"))); + + // Check that the request had no Dice request header. + if (dice_request_header_.empty()) + WaitForClosure(&chrome_signin_embedded_quit_closure_); + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, + NoDiceExtensionConsent_LaunchWebAuthFlow) { + auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>( + nullptr, browser()->profile(), https_server_.GetURL(kSigninURL), + extensions::WebAuthFlow::INTERACTIVE, + extensions::WebAuthFlow::LAUNCH_WEB_AUTH_FLOW); + web_auth_flow->Start(); + + if (dice_request_header_.empty()) + WaitForClosure(&signin_requested_quit_closure_); + + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); + + // Delete the web auth flow (uses DeleteSoon). + web_auth_flow.release()->DetachDelegateAndDelete(); + base::RunLoop().RunUntilIdle(); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, DiceExtensionConsent_GetAuthToken) { + // Signin from extension consent flow. + class DummyDelegate : public extensions::WebAuthFlow::Delegate { + public: + void OnAuthFlowFailure(extensions::WebAuthFlow::Failure failure) override {} + ~DummyDelegate() override = default; + }; + + DummyDelegate delegate; + auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>( + &delegate, browser()->profile(), https_server_.GetURL(kSigninURL), + extensions::WebAuthFlow::INTERACTIVE, + extensions::WebAuthFlow::GET_AUTH_TOKEN); + web_auth_flow->Start(); + + // Check that the token was requested and added to the token service. + SendRefreshTokenResponse(); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + + // Check that the Dice request header was sent. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + // Sync should not be enabled. + EXPECT_TRUE(GetIdentityManager() + ->GetPrimaryAccountId(signin::ConsentLevel::kSync) + .empty()); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(1, reconcilor_started_count_); + + // Delete the web auth flow (uses DeleteSoon). + web_auth_flow.release()->DetachDelegateAndDelete(); + base::RunLoop().RunUntilIdle(); +} + +// Tests that Sync is enabled if the ENABLE_SYNC response is received after the +// refresh token. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, EnableSyncAfterToken) { + EXPECT_EQ(0, reconcilor_started_count_); + + // Signin using the Chrome Sync endpoint. + browser()->signin_view_controller()->ShowSignin( + profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN, + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS); + + // Receive token. + EXPECT_FALSE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + SendRefreshTokenResponse(); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + + // Receive ENABLE_SYNC. + SendEnableSyncResponse(); + + // Check that the Dice request header was sent, with signout confirmation. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + content::WindowedNotificationObserver ntp_url_observer( + content::NOTIFICATION_LOAD_STOP, + base::BindRepeating([](const content::NotificationSource&, + const content::NotificationDetails& details) { + auto url = + content::Details<content::LoadNotificationDetails>(details)->url; + // Some test flags (e.g. ForceWebRequestProxyForTest) can change whether + // the reported NTP URL is chrome://newtab or chrome://new-tab-page. + return url == GURL(chrome::kChromeUINewTabPageURL) || + url == GURL(chrome::kChromeUINewTabURL); + })); + + WaitForSigninSucceeded(); + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(1, reconcilor_started_count_); + + // Check that the tab was navigated to the NTP. + ntp_url_observer.Wait(); + + // Dismiss the Sync confirmation UI. + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(browser())); +} + +// Tests that Sync is enabled if the ENABLE_SYNC response is received before the +// refresh token. + +// https://crbug.com/1082858 +#if (defined(OS_LINUX) || defined(OS_CHROMEOS)) && !defined(NDEBUG) +#define MAYBE_EnableSyncBeforeToken DISABLED_EnableSyncBeforeToken +#else +#define MAYBE_EnableSyncBeforeToken EnableSyncBeforeToken +#endif +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, MAYBE_EnableSyncBeforeToken) { + EXPECT_EQ(0, reconcilor_started_count_); + + ui_test_utils::UrlLoadObserver enable_sync_url_observer( + https_server_.GetURL(kEnableSyncURL), + content::NotificationService::AllSources()); + + // Signin using the Chrome Sync endpoint. + browser()->signin_view_controller()->ShowSignin( + profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN, + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS); + + // Receive ENABLE_SYNC. + SendEnableSyncResponse(); + // Wait for the page to be fully loaded. + enable_sync_url_observer.Wait(); + + // Receive token. + EXPECT_FALSE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + SendRefreshTokenResponse(); + EXPECT_TRUE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + + // Check that the Dice request header was sent, with signout confirmation. + std::string client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id(); + EXPECT_EQ(base::StringPrintf("version=%s,client_id=%s,device_id=%s," + "signin_mode=all_accounts," + "signout_mode=show_confirmation", + signin::kDiceProtocolVersion, client_id.c_str(), + GetDeviceId().c_str()), + dice_request_header_); + + ui_test_utils::UrlLoadObserver ntp_url_observer( + GURL(chrome::kChromeUINewTabURL), + content::NotificationService::AllSources()); + + WaitForSigninSucceeded(); + EXPECT_EQ(GetMainAccountID(), GetIdentityManager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync)); + + EXPECT_EQ(1, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(1); + EXPECT_EQ(1, reconcilor_started_count_); + + // Check that the tab was navigated to the NTP. + ntp_url_observer.Wait(); + + // Dismiss the Sync confirmation UI. + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog(browser())); +} + +// Tests that turning off Dice via preferences works when singed out. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_SignedOut) { + ASSERT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + // Turn off Dice for this profile. + browser()->profile()->GetPrefs()->SetBoolean( + prefs::kSigninAllowedOnNextStartup, false); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_SignedOut) { + // Check that Dice is disabled. + EXPECT_FALSE( + browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean( + prefs::kSigninAllowedOnNextStartup)); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + EXPECT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + + // Navigate to Gaia and sign in. + NavigateToURL(kSigninURL); + // Check that the Dice request header was not sent. + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); +} + +// Tests that turning off Dice via preferences works when signed in without sync +// consent. +// +// Regression test for crbug/1254325 +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_NotOptedIntoSync) { + SetupSignedInAccounts(signin::ConsentLevel::kSignin); + + ASSERT_TRUE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + ASSERT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + // Turn off Dice for this profile. + browser()->profile()->GetPrefs()->SetBoolean( + prefs::kSigninAllowedOnNextStartup, false); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_NotOptedIntoSync) { + // Check that Dice is disabled. + EXPECT_FALSE( + browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean( + prefs::kSigninAllowedOnNextStartup)); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + EXPECT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + EXPECT_FALSE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_TRUE(GetIdentityManager()->GetAccountsWithRefreshTokens().empty()); + + // Navigate to Gaia and sign in. + NavigateToURL(kSigninURL); + // Check that the Dice request header was not sent. + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); +} + +// Tests that turning off Dice via preferences works when signed in with sync +// consent +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, PRE_TurnOffDice_OptedIntoSync) { + // Sign the profile in and turn sync on. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + syncer::SyncPrefs(browser()->profile()->GetPrefs()).SetFirstSetupComplete(); + + ASSERT_TRUE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + ASSERT_TRUE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + // Turn off Dice for this profile. + browser()->profile()->GetPrefs()->SetBoolean( + prefs::kSigninAllowedOnNextStartup, false); +} + +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, TurnOffDice_OptedIntoSync) { + EXPECT_FALSE( + browser()->profile()->GetPrefs()->GetBoolean(prefs::kSigninAllowed)); + EXPECT_FALSE(browser()->profile()->GetPrefs()->GetBoolean( + prefs::kSigninAllowedOnNextStartup)); + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + browser()->profile())); + + EXPECT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + EXPECT_FALSE( + GetIdentityManager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + EXPECT_FALSE( + GetIdentityManager()->HasAccountWithRefreshToken(GetMainAccountID())); + EXPECT_TRUE(GetIdentityManager()->GetAccountsWithRefreshTokens().empty()); + + // Navigate to Gaia and sign in. + NavigateToURL(kSigninURL); + // Check that the Dice request header was not sent. + EXPECT_EQ(kNoDiceRequestHeader, dice_request_header_); + EXPECT_EQ(0, reconcilor_blocked_count_); + WaitForReconcilorUnblockedCount(0); +} + +// Checks that Dice is disabled in incognito mode. +IN_PROC_BROWSER_TEST_F(DiceBrowserTest, Incognito) { + Browser* incognito_browser = Browser::Create(Browser::CreateParams( + browser()->profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true), + true)); + + // Check that Dice is disabled. + EXPECT_FALSE(AccountConsistencyModeManager::IsDiceEnabledForProfile( + incognito_browser->profile())); +} + +// This test is not specifically related to DICE, but it extends +// |DiceBrowserTest| for convenience. +class DiceManageAccountBrowserTest : public DiceBrowserTest { + public: + DiceManageAccountBrowserTest() + : DiceBrowserTest(kMainManagedEmail), + // Skip showing the error message box to avoid freezing the main thread. + skip_message_box_auto_reset_( + &chrome::internal::g_should_skip_message_box_for_test, + true), + // Force the policy component to prohibit clearing the primary account + // even when the policy core component is not initialized. + prohibit_sigout_auto_reset_( + &policy::internal::g_force_prohibit_signout_for_tests, + true) {} + + void SetUp() override { +#if defined(OS_WIN) + // Shortcut deletion delays tests shutdown on Win-7 and results in time out. + // See crbug.com/1073451. + AppShortcutManager::SuppressShortcutsForTesting(); +#endif + DiceBrowserTest::SetUp(); + } + + protected: + base::AutoReset<bool> skip_message_box_auto_reset_; + base::AutoReset<bool> prohibit_sigout_auto_reset_; + unsigned int number_of_profiles_added_ = 0; +}; + +// Tests that prohiting sign-in on startup for a managed profile clears the +// profile directory on next start-up. +IN_PROC_BROWSER_TEST_F(DiceManageAccountBrowserTest, + PRE_ClearManagedProfileOnStartup) { + // Ensure that there are not deleted profiles before running this test. + PrefService* local_state = g_browser_process->local_state(); + DCHECK(local_state); + const base::ListValue* deleted_profiles = + local_state->GetList(prefs::kProfilesDeleted); + ASSERT_TRUE(deleted_profiles); + ASSERT_TRUE(deleted_profiles->GetList().empty()); + + // Sign the profile in. + SetupSignedInAccounts(signin::ConsentLevel::kSync); + + // Prohibit sign-in on next start-up. + browser()->profile()->GetPrefs()->SetBoolean( + prefs::kSigninAllowedOnNextStartup, false); +} + +IN_PROC_BROWSER_TEST_F(DiceManageAccountBrowserTest, + ClearManagedProfileOnStartup) { + // Initial profile should have been deleted as sign-in and sign out were no + // longer allowed. + PrefService* local_state = g_browser_process->local_state(); + DCHECK(local_state); + const base::ListValue* deleted_profiles = + local_state->GetList(prefs::kProfilesDeleted); + EXPECT_TRUE(deleted_profiles); + EXPECT_EQ(1U, deleted_profiles->GetList().size()); + + content::RunAllTasksUntilIdle(); + + // Verify that there is an active profile. + Profile* initial_profile = browser()->profile(); + EXPECT_EQ(1U, g_browser_process->profile_manager()->GetNumberOfProfiles()); + EXPECT_EQ(g_browser_process->profile_manager()->GetLastUsedProfile(), + initial_profile); +} diff --git a/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc new file mode 100644 index 00000000000..ecd8893f92b --- /dev/null +++ b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.cc @@ -0,0 +1,179 @@ +// 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. + +#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h" + +#include <algorithm> +#include <vector> + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/browser_navigator.h" +#include "chrome/browser/ui/browser_navigator_params.h" +#include "chrome/common/webui_url_constants.h" +#include "components/signin/public/base/multilogin_parameters.h" +#include "components/signin/public/identity_manager/accounts_cookie_mutator.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/set_accounts_in_cookie_result.h" +#include "content/public/browser/web_contents.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "url/gurl.h" + +namespace { + +// Returns true if |account_id| is signed in the cookies. +bool CookieInfoContains(const signin::AccountsInCookieJarInfo& cookie_info, + const CoreAccountId& account_id) { + const std::vector<gaia::ListedAccount>& accounts = + cookie_info.signed_in_accounts; + return std::find_if(accounts.begin(), accounts.end(), + [&account_id](const gaia::ListedAccount& account) { + return account.id == account_id; + }) != accounts.end(); +} + +} // namespace + +DiceInterceptedSessionStartupHelper::DiceInterceptedSessionStartupHelper( + Profile* profile, + bool is_new_profile, + CoreAccountId account_id, + content::WebContents* tab_to_move) + : profile_(profile), + use_multilogin_(is_new_profile), + account_id_(account_id) { + if (tab_to_move) + web_contents_ = tab_to_move->GetWeakPtr(); +} + +DiceInterceptedSessionStartupHelper::~DiceInterceptedSessionStartupHelper() = + default; + +void DiceInterceptedSessionStartupHelper::Startup(base::OnceClosure callback) { + callback_ = std::move(callback); + + // Wait until the account is set in cookies of the newly created profile + // before opening the URL, so that the user is signed-in in content area. If + // the account is still not in the cookie after some timeout, proceed without + // cookies, so that the user can at least take some action in the new profile. + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile_); + signin::AccountsInCookieJarInfo cookie_info = + identity_manager->GetAccountsInCookieJar(); + if (cookie_info.accounts_are_fresh && + CookieInfoContains(cookie_info, account_id_)) { + MoveTab(); + } else { + // Set the timeout. + on_cookie_update_timeout_.Reset(base::BindOnce( + &DiceInterceptedSessionStartupHelper::MoveTab, base::Unretained(this))); + // Adding accounts to the cookies can be an expensive operation. In + // particular the ExternalCCResult fetch may time out after multiple seconds + // (see kExternalCCResultTimeoutSeconds and https://crbug.com/750316#c37). + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, on_cookie_update_timeout_.callback(), base::Seconds(12)); + + accounts_in_cookie_observer_.Observe(identity_manager); + if (use_multilogin_) + StartupMultilogin(identity_manager); + else + StartupReconcilor(identity_manager); + } +} + +void DiceInterceptedSessionStartupHelper::OnAccountsInCookieUpdated( + const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, + const GoogleServiceAuthError& error) { + if (error != GoogleServiceAuthError::AuthErrorNone()) + return; + if (!accounts_in_cookie_jar_info.accounts_are_fresh) + return; + if (!CookieInfoContains(accounts_in_cookie_jar_info, account_id_)) + return; + + MoveTab(); +} + +void DiceInterceptedSessionStartupHelper::OnStateChanged( + signin_metrics::AccountReconcilorState state) { + DCHECK(!use_multilogin_); + if (state == signin_metrics::ACCOUNT_RECONCILOR_ERROR) { + reconcile_error_encountered_ = true; + return; + } + + // TODO(https://crbug.com/1051864): remove this when the cookie updates are + // correctly sent after reconciliation. + if (state == signin_metrics::ACCOUNT_RECONCILOR_OK) { + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile_); + // GetAccountsInCookieJar() automatically re-schedules a /ListAccounts call + // if the cookie is not fresh. + signin::AccountsInCookieJarInfo cookie_info = + identity_manager->GetAccountsInCookieJar(); + OnAccountsInCookieUpdated(cookie_info, + GoogleServiceAuthError::AuthErrorNone()); + } +} + +void DiceInterceptedSessionStartupHelper::StartupMultilogin( + signin::IdentityManager* identity_manager) { + // Lock the reconcilor to avoid making multiple multilogin calls. + reconcilor_lock_ = std::make_unique<AccountReconcilor::Lock>( + AccountReconcilorFactory::GetForProfile(profile_)); + + // Start the multilogin call. + signin::MultiloginParameters params = { + /*mode=*/gaia::MultiloginMode::MULTILOGIN_UPDATE_COOKIE_ACCOUNTS_ORDER, + /*accounts_to_send=*/{account_id_}}; + identity_manager->GetAccountsCookieMutator()->SetAccountsInCookie( + params, gaia::GaiaSource::kChrome, + base::BindOnce( + &DiceInterceptedSessionStartupHelper::OnSetAccountInCookieCompleted, + weak_factory_.GetWeakPtr())); +} + +void DiceInterceptedSessionStartupHelper::StartupReconcilor( + signin::IdentityManager* identity_manager) { + // TODO(https://crbug.com/1051864): cookie notifications are not triggered + // when the account is added by the reconcilor. Observe the reconcilor and + // re-trigger the cookie update when it completes. + reconcilor_observer_.Observe( + AccountReconcilorFactory::GetForProfile(profile_)); + identity_manager->GetAccountsCookieMutator()->TriggerCookieJarUpdate(); +} + +void DiceInterceptedSessionStartupHelper::OnSetAccountInCookieCompleted( + signin::SetAccountsInCookieResult result) { + DCHECK(use_multilogin_); + MoveTab(); +} + +void DiceInterceptedSessionStartupHelper::MoveTab() { + accounts_in_cookie_observer_.Reset(); + reconcilor_observer_.Reset(); + on_cookie_update_timeout_.Cancel(); + reconcilor_lock_.reset(); + + GURL url_to_open = GURL(chrome::kChromeUINewTabURL); + // If the intercepted web contents is still alive, close it now. + if (web_contents_) { + url_to_open = web_contents_->GetURL(); + web_contents_->Close(); + } + + // Open a new browser. + NavigateParams params(profile_, url_to_open, + ui::PAGE_TRANSITION_AUTO_BOOKMARK); + Navigate(¶ms); + + if (callback_) + std::move(callback_).Run(); +} diff --git a/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h new file mode 100644 index 00000000000..2895a90d66b --- /dev/null +++ b/chromium/chrome/browser/signin/dice_intercepted_session_startup_helper.h @@ -0,0 +1,102 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_ + +#include "base/callback_forward.h" +#include "base/cancelable_callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/scoped_observation.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "content/public/browser/web_contents_observer.h" +#include "google_apis/gaia/core_account_id.h" + +namespace content { +class WebContents; +} + +namespace signin { +struct AccountsInCookieJarInfo; +class IdentityManager; +enum class SetAccountsInCookieResult; +} + +class GoogleServiceAuthError; +class Profile; + +// Called when the user accepted the dice signin interception and the new +// profile has been created. Creates a new browser and moves the intercepted tab +// to the new browser. +// It is assumed that the account is already in the profile, but not necessarily +// in the content area (cookies). +class DiceInterceptedSessionStartupHelper + : public signin::IdentityManager::Observer, + public AccountReconcilor::Observer { + public: + // |profile| is the new profile that was created after signin interception. + // |account_id| is the main account for the profile, it's already in the + // profile. + // |tab_to_move| is the tab where the interception happened, in the source + // profile. + DiceInterceptedSessionStartupHelper(Profile* profile, + bool is_new_profile, + CoreAccountId account_id, + content::WebContents* tab_to_move); + + ~DiceInterceptedSessionStartupHelper() override; + + DiceInterceptedSessionStartupHelper( + const DiceInterceptedSessionStartupHelper&) = delete; + DiceInterceptedSessionStartupHelper& operator=( + const DiceInterceptedSessionStartupHelper&) = delete; + + // Start up the session. Can only be called once. + void Startup(base::OnceClosure callback); + + // signin::IdentityManager::Observer: + void OnAccountsInCookieUpdated( + const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, + const GoogleServiceAuthError& error) override; + + // AccountReconcilor::Observer: + void OnStateChanged(signin_metrics::AccountReconcilorState state) override; + + private: + // For new profiles, the account is added directly using multilogin. + void StartupMultilogin(signin::IdentityManager* identity_manager); + + // For existing profiles, simply wait for the reconcilor to update the + // accounts. + void StartupReconcilor(signin::IdentityManager* identity_manager); + + // Called when multilogin completes. + void OnSetAccountInCookieCompleted(signin::SetAccountsInCookieResult result); + + // Creates a browser with a new tab, and closes the intercepted tab if it's + // still open. + void MoveTab(); + + const raw_ptr<Profile> profile_; + base::WeakPtr<content::WebContents> web_contents_; + bool use_multilogin_; + CoreAccountId account_id_; + base::OnceClosure callback_; + bool reconcile_error_encountered_ = false; + base::ScopedObservation<signin::IdentityManager, + signin::IdentityManager::Observer> + accounts_in_cookie_observer_{this}; + base::ScopedObservation<AccountReconcilor, AccountReconcilor::Observer> + reconcilor_observer_{this}; + std::unique_ptr<AccountReconcilor::Lock> reconcilor_lock_; + // Timeout while waiting for the account to be added to the cookies in the new + // profile. + base::CancelableOnceCallback<void()> on_cookie_update_timeout_; + + base::WeakPtrFactory<DiceInterceptedSessionStartupHelper> weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_INTERCEPTED_SESSION_STARTUP_HELPER_H_ diff --git a/chromium/chrome/browser/signin/dice_response_handler.cc b/chromium/chrome/browser/signin/dice_response_handler.cc new file mode 100644 index 00000000000..88d7563846f --- /dev/null +++ b/chromium/chrome/browser/signin/dice_response_handler.cc @@ -0,0 +1,426 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/dice_response_handler.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/memory/singleton.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/stringprintf.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profiles_state.h" +#include "chrome/browser/signin/about_signin_internals_factory.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/webui/profile_helper.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "components/signin/core/browser/about_signin_internals.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/signin_client.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "google_apis/gaia/google_service_auth_error.h" + +const int kDiceTokenFetchTimeoutSeconds = 10; +// Timeout for locking the account reconcilor when +// there was OAuth outage in Dice. +const int kLockAccountReconcilorTimeoutHours = 12; + +const base::Feature kSupportOAuthOutageInDice{"SupportOAuthOutageInDice", + base::FEATURE_ENABLED_BY_DEFAULT}; + +namespace { + +// The UMA histograms that logs events related to Dice responses. +const char kDiceResponseHeaderHistogram[] = "Signin.DiceResponseHeader"; +const char kDiceTokenFetchResultHistogram[] = "Signin.DiceTokenFetchResult"; + +// Used for UMA. Do not reorder, append new values at the end. +enum DiceResponseHeader { + // Received a signin header. + kSignin = 0, + // Received a signout header including the Chrome primary account. + kSignoutPrimary = 1, + // Received a signout header for other account(s). + kSignoutSecondary = 2, + // Received a "EnableSync" header. + kEnableSync = 3, + + kDiceResponseHeaderCount +}; + +// Used for UMA. Do not reorder, append new values at the end. +enum DiceTokenFetchResult { + // The token fetch succeeded. + kFetchSuccess = 0, + // The token fetch was aborted. For example, if another request for the same + // account is already in flight. + kFetchAbort = 1, + // The token fetch failed because Gaia responsed with an error. + kFetchFailure = 2, + // The token fetch failed because no response was received from Gaia. + kFetchTimeout = 3, + + kDiceTokenFetchResultCount +}; + +class DiceResponseHandlerFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns an instance of the factory singleton. + static DiceResponseHandlerFactory* GetInstance() { + return base::Singleton<DiceResponseHandlerFactory>::get(); + } + + static DiceResponseHandler* GetForProfile(Profile* profile) { + return static_cast<DiceResponseHandler*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); + } + + private: + friend struct base::DefaultSingletonTraits<DiceResponseHandlerFactory>; + + DiceResponseHandlerFactory() + : BrowserContextKeyedServiceFactory( + "DiceResponseHandler", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(AboutSigninInternalsFactory::GetInstance()); + DependsOn(AccountReconcilorFactory::GetInstance()); + DependsOn(ChromeSigninClientFactory::GetInstance()); + DependsOn(IdentityManagerFactory::GetInstance()); + } + + ~DiceResponseHandlerFactory() override {} + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override { + if (context->IsOffTheRecord()) + return nullptr; + + Profile* profile = static_cast<Profile*>(context); + return new DiceResponseHandler( + ChromeSigninClientFactory::GetForProfile(profile), + IdentityManagerFactory::GetForProfile(profile), + AccountReconcilorFactory::GetForProfile(profile), + AboutSigninInternalsFactory::GetForProfile(profile), + profile->GetPath()); + } +}; + +// Histogram macros expand to a lot of code, so it is better to wrap them in +// functions. + +void RecordDiceResponseHeader(DiceResponseHeader header) { + UMA_HISTOGRAM_ENUMERATION(kDiceResponseHeaderHistogram, header, + kDiceResponseHeaderCount); +} + +void RecordDiceFetchTokenResult(DiceTokenFetchResult result) { + UMA_HISTOGRAM_ENUMERATION(kDiceTokenFetchResultHistogram, result, + kDiceTokenFetchResultCount); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// DiceTokenFetcher +//////////////////////////////////////////////////////////////////////////////// + +DiceResponseHandler::DiceTokenFetcher::DiceTokenFetcher( + const std::string& gaia_id, + const std::string& email, + const std::string& authorization_code, + SigninClient* signin_client, + AccountReconcilor* account_reconcilor, + std::unique_ptr<ProcessDiceHeaderDelegate> delegate, + DiceResponseHandler* dice_response_handler) + : gaia_id_(gaia_id), + email_(email), + authorization_code_(authorization_code), + delegate_(std::move(delegate)), + dice_response_handler_(dice_response_handler), + timeout_closure_( + base::BindOnce(&DiceResponseHandler::DiceTokenFetcher::OnTimeout, + base::Unretained(this))), + should_enable_sync_(false) { + DCHECK(dice_response_handler_); + account_reconcilor_lock_ = + std::make_unique<AccountReconcilor::Lock>(account_reconcilor); + gaia_auth_fetcher_ = + signin_client->CreateGaiaAuthFetcher(this, gaia::GaiaSource::kChrome); + VLOG(1) << "Start fetching token for account: " << email; + gaia_auth_fetcher_->StartAuthCodeForOAuth2TokenExchange(authorization_code_); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, timeout_closure_.callback(), + base::Seconds(kDiceTokenFetchTimeoutSeconds)); +} + +DiceResponseHandler::DiceTokenFetcher::~DiceTokenFetcher() {} + +void DiceResponseHandler::DiceTokenFetcher::OnTimeout() { + RecordDiceFetchTokenResult(kFetchTimeout); + gaia_auth_fetcher_.reset(); + timeout_closure_.Cancel(); + dice_response_handler_->OnTokenExchangeFailure( + this, GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED)); + // |this| may be deleted at this point. +} + +void DiceResponseHandler::DiceTokenFetcher::OnClientOAuthSuccess( + const GaiaAuthConsumer::ClientOAuthResult& result) { + RecordDiceFetchTokenResult(kFetchSuccess); + gaia_auth_fetcher_.reset(); + timeout_closure_.Cancel(); + dice_response_handler_->OnTokenExchangeSuccess( + this, result.refresh_token, result.is_under_advanced_protection); + // |this| may be deleted at this point. +} + +void DiceResponseHandler::DiceTokenFetcher::OnClientOAuthFailure( + const GoogleServiceAuthError& error) { + RecordDiceFetchTokenResult(kFetchFailure); + gaia_auth_fetcher_.reset(); + timeout_closure_.Cancel(); + dice_response_handler_->OnTokenExchangeFailure(this, error); + // |this| may be deleted at this point. +} + +//////////////////////////////////////////////////////////////////////////////// +// DiceResponseHandler +//////////////////////////////////////////////////////////////////////////////// + +// static +DiceResponseHandler* DiceResponseHandler::GetForProfile(Profile* profile) { + return DiceResponseHandlerFactory::GetForProfile(profile); +} + +DiceResponseHandler::DiceResponseHandler( + SigninClient* signin_client, + signin::IdentityManager* identity_manager, + AccountReconcilor* account_reconcilor, + AboutSigninInternals* about_signin_internals, + const base::FilePath& profile_path) + : signin_client_(signin_client), + identity_manager_(identity_manager), + account_reconcilor_(account_reconcilor), + about_signin_internals_(about_signin_internals), + profile_path_(profile_path) { + DCHECK(signin_client_); + DCHECK(identity_manager_); + DCHECK(account_reconcilor_); + DCHECK(about_signin_internals_); +} + +DiceResponseHandler::~DiceResponseHandler() {} + +void DiceResponseHandler::ProcessDiceHeader( + const signin::DiceResponseParams& dice_params, + std::unique_ptr<ProcessDiceHeaderDelegate> delegate) { + DCHECK(delegate); + switch (dice_params.user_intention) { + case signin::DiceAction::SIGNIN: { + const signin::DiceResponseParams::AccountInfo& info = + dice_params.signin_info->account_info; + ProcessDiceSigninHeader( + info.gaia_id, info.email, dice_params.signin_info->authorization_code, + dice_params.signin_info->no_authorization_code, std::move(delegate)); + return; + } + case signin::DiceAction::ENABLE_SYNC: { + const signin::DiceResponseParams::AccountInfo& info = + dice_params.enable_sync_info->account_info; + ProcessEnableSyncHeader(info.gaia_id, info.email, std::move(delegate)); + return; + } + case signin::DiceAction::SIGNOUT: + DCHECK_GT(dice_params.signout_info->account_infos.size(), 0u); + ProcessDiceSignoutHeader(dice_params.signout_info->account_infos); + return; + case signin::DiceAction::NONE: + NOTREACHED() << "Invalid Dice response parameters."; + return; + } + NOTREACHED(); +} + +size_t DiceResponseHandler::GetPendingDiceTokenFetchersCountForTesting() const { + return token_fetchers_.size(); +} + +void DiceResponseHandler::OnTimeoutUnlockReconcilor() { + lock_.reset(); +} + +void DiceResponseHandler::SetTaskRunner( + scoped_refptr<base::SequencedTaskRunner> task_runner) { + task_runner_ = std::move(task_runner); +} + +void DiceResponseHandler::ProcessDiceSigninHeader( + const std::string& gaia_id, + const std::string& email, + const std::string& authorization_code, + bool no_authorization_code, + std::unique_ptr<ProcessDiceHeaderDelegate> delegate) { + if (no_authorization_code) { + if (base::FeatureList::IsEnabled(kSupportOAuthOutageInDice)) { + lock_ = std::make_unique<AccountReconcilor::Lock>(account_reconcilor_); + about_signin_internals_->OnRefreshTokenReceived( + "Missing authorization code due to OAuth outage in Dice."); + if (!timer_) { + timer_ = std::make_unique<base::OneShotTimer>(); + if (task_runner_) + timer_->SetTaskRunner(task_runner_); + } + // If there is already another lock, the timer will be reset and + // we'll wait another full timeout. + timer_->Start( + FROM_HERE, base::Hours(kLockAccountReconcilorTimeoutHours), + base::BindOnce(&DiceResponseHandler::OnTimeoutUnlockReconcilor, + base::Unretained(this))); + } + return; + } + + DCHECK(!gaia_id.empty()); + DCHECK(!email.empty()); + DCHECK(!authorization_code.empty()); + VLOG(1) << "Start processing Dice signin response"; + RecordDiceResponseHeader(kSignin); + + for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) { + if ((it->get()->gaia_id() == gaia_id) && (it->get()->email() == email) && + (it->get()->authorization_code() == authorization_code)) { + RecordDiceFetchTokenResult(kFetchAbort); + return; // There is already a request in flight with the same parameters. + } + } + token_fetchers_.push_back(std::make_unique<DiceTokenFetcher>( + gaia_id, email, authorization_code, signin_client_, account_reconcilor_, + std::move(delegate), this)); +} + +void DiceResponseHandler::ProcessEnableSyncHeader( + const std::string& gaia_id, + const std::string& email, + std::unique_ptr<ProcessDiceHeaderDelegate> delegate) { + VLOG(1) << "Start processing Dice enable sync response"; + RecordDiceResponseHeader(kEnableSync); + for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) { + DiceTokenFetcher* fetcher = it->get(); + if (fetcher->gaia_id() == gaia_id) { + DCHECK(gaia::AreEmailsSame(fetcher->email(), email)); + // If there is a fetch in progress for a resfresh token for the given + // account, then simply mark it to enable sync after the refresh token is + // available. + fetcher->set_should_enable_sync(true); + return; // There is already a request in flight with the same parameters. + } + } + CoreAccountId account_id = + identity_manager_->PickAccountIdForAccount(gaia_id, email); + delegate->EnableSync(account_id); +} + +void DiceResponseHandler::ProcessDiceSignoutHeader( + const std::vector<signin::DiceResponseParams::AccountInfo>& account_infos) { + VLOG(1) << "Start processing Dice signout response"; + + CoreAccountId primary_account = + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync); + bool primary_account_signed_out = false; + auto* accounts_mutator = identity_manager_->GetAccountsMutator(); + for (const auto& account_info : account_infos) { + CoreAccountId signed_out_account = + identity_manager_->PickAccountIdForAccount(account_info.gaia_id, + account_info.email); + if (signed_out_account == primary_account) { + primary_account_signed_out = true; + RecordDiceResponseHeader(kSignoutPrimary); + + // Put the account in error state. + accounts_mutator->InvalidateRefreshTokenForPrimaryAccount( + signin_metrics::SourceForRefreshTokenOperation:: + kDiceResponseHandler_Signout); + } else { + accounts_mutator->RemoveAccount( + signed_out_account, signin_metrics::SourceForRefreshTokenOperation:: + kDiceResponseHandler_Signout); + } + + // If a token fetch is in flight for the same account, cancel it. + for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) { + CoreAccountId token_fetcher_account_id = + identity_manager_->PickAccountIdForAccount(it->get()->gaia_id(), + it->get()->email()); + if (token_fetcher_account_id == signed_out_account) { + token_fetchers_.erase(it); + break; + } + } + } + + if (!primary_account_signed_out) + RecordDiceResponseHeader(kSignoutSecondary); +} + +void DiceResponseHandler::DeleteTokenFetcher(DiceTokenFetcher* token_fetcher) { + for (auto it = token_fetchers_.begin(); it != token_fetchers_.end(); ++it) { + if (it->get() == token_fetcher) { + token_fetchers_.erase(it); + return; + } + } + NOTREACHED(); +} + +void DiceResponseHandler::OnTokenExchangeSuccess( + DiceTokenFetcher* token_fetcher, + const std::string& refresh_token, + bool is_under_advanced_protection) { + const std::string& email = token_fetcher->email(); + const std::string& gaia_id = token_fetcher->gaia_id(); + VLOG(1) << "[Dice] OAuth success for email " << email; + bool should_enable_sync = token_fetcher->should_enable_sync(); + CoreAccountId account_id = + identity_manager_->PickAccountIdForAccount(gaia_id, email); + bool is_new_account = + !identity_manager_->HasAccountWithRefreshToken(account_id); + identity_manager_->GetAccountsMutator()->AddOrUpdateAccount( + gaia_id, email, refresh_token, is_under_advanced_protection, + signin_metrics::SourceForRefreshTokenOperation:: + kDiceResponseHandler_Signin); + about_signin_internals_->OnRefreshTokenReceived( + base::StringPrintf("Successful (%s)", account_id.ToString().c_str())); + token_fetcher->delegate()->HandleTokenExchangeSuccess(account_id, + is_new_account); + if (should_enable_sync) + token_fetcher->delegate()->EnableSync(account_id); + + DeleteTokenFetcher(token_fetcher); +} + +void DiceResponseHandler::OnTokenExchangeFailure( + DiceTokenFetcher* token_fetcher, + const GoogleServiceAuthError& error) { + const std::string& email = token_fetcher->email(); + const std::string& gaia_id = token_fetcher->gaia_id(); + CoreAccountId account_id = + identity_manager_->PickAccountIdForAccount(gaia_id, email); + about_signin_internals_->OnRefreshTokenReceived( + base::StringPrintf("Failure (%s)", account_id.ToString().c_str())); + token_fetcher->delegate()->HandleTokenExchangeFailure(email, error); + + DeleteTokenFetcher(token_fetcher); +} diff --git a/chromium/chrome/browser/signin/dice_response_handler.h b/chromium/chrome/browser/signin/dice_response_handler.h new file mode 100644 index 00000000000..2e90c9ddc11 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_response_handler.h @@ -0,0 +1,187 @@ +// Copyright 2017 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. + +#ifndef CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_ +#define CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_ + +#include <memory> +#include <string> +#include <vector> + +#include "base/cancelable_callback.h" +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/scoped_refptr.h" +#include "base/task/sequenced_task_runner.h" +#include "base/timer/timer.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "google_apis/gaia/gaia_auth_consumer.h" + +class AboutSigninInternals; +class GaiaAuthFetcher; +class GoogleServiceAuthError; +class SigninClient; +class Profile; + +namespace signin { +class IdentityManager; +} + +// Exposed for testing. +extern const int kDiceTokenFetchTimeoutSeconds; +// Exposed for testing. +extern const int kLockAccountReconcilorTimeoutHours; +extern const base::Feature kSupportOAuthOutageInDice; + +// Delegate interface for processing a dice request. +class ProcessDiceHeaderDelegate { + public: + virtual ~ProcessDiceHeaderDelegate() = default; + + // Called when a token was successfully exchanged. + // Called after the account was seeded in the account tracker service and + // after the refresh token was fetched and updated in the token service. + // |is_new_account| is true if the account was added to Chrome (it is not a + // re-auth). + virtual void HandleTokenExchangeSuccess(CoreAccountId account_id, + bool is_new_account) = 0; + + // Asks the delegate to enable sync for the |account_id|. + // Called after the account was seeded in the account tracker service and + // after the refresh token was fetched and updated in the token service. + virtual void EnableSync(const CoreAccountId& account_id) = 0; + + // Handles a failure in the token exchange (i.e. shows the error to the user). + virtual void HandleTokenExchangeFailure( + const std::string& email, + const GoogleServiceAuthError& error) = 0; +}; + +// Processes the Dice responses from Gaia. +class DiceResponseHandler : public KeyedService { + public: + // Returns the DiceResponseHandler associated with this profile. + // May return nullptr if there is none (e.g. in incognito). + static DiceResponseHandler* GetForProfile(Profile* profile); + + DiceResponseHandler(SigninClient* signin_client, + signin::IdentityManager* identity_manager, + AccountReconcilor* account_reconcilor, + AboutSigninInternals* about_signin_internals, + const base::FilePath& profile_path_); + + DiceResponseHandler(const DiceResponseHandler&) = delete; + DiceResponseHandler& operator=(const DiceResponseHandler&) = delete; + + ~DiceResponseHandler() override; + + // Must be called when receiving a Dice response header. + void ProcessDiceHeader(const signin::DiceResponseParams& dice_params, + std::unique_ptr<ProcessDiceHeaderDelegate> delegate); + + // Returns the number of pending DiceTokenFetchers. Exposed for testing. + size_t GetPendingDiceTokenFetchersCountForTesting() const; + + // Sets |task_runner_| for testing. + void SetTaskRunner(scoped_refptr<base::SequencedTaskRunner> task_runner); + + private: + // Helper class to fetch a refresh token from an authorization code. + class DiceTokenFetcher : public GaiaAuthConsumer { + public: + DiceTokenFetcher(const std::string& gaia_id, + const std::string& email, + const std::string& authorization_code, + SigninClient* signin_client, + AccountReconcilor* account_reconcilor, + std::unique_ptr<ProcessDiceHeaderDelegate> delegate, + DiceResponseHandler* dice_response_handler); + + DiceTokenFetcher(const DiceTokenFetcher&) = delete; + DiceTokenFetcher& operator=(const DiceTokenFetcher&) = delete; + + ~DiceTokenFetcher() override; + + const std::string& gaia_id() const { return gaia_id_; } + const std::string& email() const { return email_; } + const std::string& authorization_code() const { + return authorization_code_; + } + bool should_enable_sync() const { return should_enable_sync_; } + void set_should_enable_sync(bool should_enable_sync) { + should_enable_sync_ = should_enable_sync; + } + ProcessDiceHeaderDelegate* delegate() { return delegate_.get(); } + + private: + // Called by |timeout_closure_| when the request times out. + void OnTimeout(); + + // GaiaAuthConsumer implementation: + void OnClientOAuthSuccess( + const GaiaAuthConsumer::ClientOAuthResult& result) override; + void OnClientOAuthFailure(const GoogleServiceAuthError& error) override; + + // Lock the account reconcilor while tokens are being fetched. + std::unique_ptr<AccountReconcilor::Lock> account_reconcilor_lock_; + + std::string gaia_id_; + std::string email_; + std::string authorization_code_; + std::unique_ptr<ProcessDiceHeaderDelegate> delegate_; + raw_ptr<DiceResponseHandler> dice_response_handler_; + base::CancelableOnceClosure timeout_closure_; + bool should_enable_sync_; + std::unique_ptr<GaiaAuthFetcher> gaia_auth_fetcher_; + }; + + // Deletes the token fetcher. + void DeleteTokenFetcher(DiceTokenFetcher* token_fetcher); + + // Process the Dice signin action. + void ProcessDiceSigninHeader( + const std::string& gaia_id, + const std::string& email, + const std::string& authorization_code, + bool no_authorization_code, + std::unique_ptr<ProcessDiceHeaderDelegate> delegate); + + // Process the Dice enable sync action. + void ProcessEnableSyncHeader( + const std::string& gaia_id, + const std::string& email, + std::unique_ptr<ProcessDiceHeaderDelegate> delegate); + + // Process the Dice signout action. + void ProcessDiceSignoutHeader( + const std::vector<signin::DiceResponseParams::AccountInfo>& + account_infos); + + // Called after exchanging an OAuth 2.0 authorization code for a refresh token + // after DiceAction::SIGNIN. + void OnTokenExchangeSuccess(DiceTokenFetcher* token_fetcher, + const std::string& refresh_token, + bool is_under_advanced_protection); + void OnTokenExchangeFailure(DiceTokenFetcher* token_fetcher, + const GoogleServiceAuthError& error); + // Called to unlock the reconcilor after a SLO outage. + void OnTimeoutUnlockReconcilor(); + + raw_ptr<SigninClient> signin_client_; + raw_ptr<signin::IdentityManager> identity_manager_; + raw_ptr<AccountReconcilor> account_reconcilor_; + raw_ptr<AboutSigninInternals> about_signin_internals_; + base::FilePath profile_path_; + std::vector<std::unique_ptr<DiceTokenFetcher>> token_fetchers_; + // Lock the account reconcilor for kLockAccountReconcilorTimeoutHours + // when there was OAuth outage in Dice. + std::unique_ptr<AccountReconcilor::Lock> lock_; + std::unique_ptr<base::OneShotTimer> timer_; + scoped_refptr<base::SequencedTaskRunner> task_runner_; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_RESPONSE_HANDLER_H_ diff --git a/chromium/chrome/browser/signin/dice_response_handler_unittest.cc b/chromium/chrome/browser/signin/dice_response_handler_unittest.cc new file mode 100644 index 00000000000..437f4a4fb20 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_response_handler_unittest.cc @@ -0,0 +1,820 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/dice_response_handler.h" + +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/check.h" +#include "base/command_line.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/notreached.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/task_environment.h" +#include "base/time/time.h" +#include "chrome/test/base/testing_profile.h" +#include "components/signin/core/browser/about_signin_internals.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/core/browser/dice_account_reconcilor_delegate.h" +#include "components/signin/core/browser/signin_error_controller.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/test_signin_client.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using signin::DiceAction; +using signin::DiceResponseParams; + +namespace { + +const char kAuthorizationCode[] = "authorization_code"; +const char kEmail[] = "test@email.com"; +const int kSessionIndex = 42; + +// TestSigninClient implementation that intercepts the GaiaAuthConsumer and +// replaces it by a dummy one. +class DiceTestSigninClient : public TestSigninClient, public GaiaAuthConsumer { + public: + explicit DiceTestSigninClient(PrefService* pref_service) + : TestSigninClient(pref_service), consumer_(nullptr) {} + + DiceTestSigninClient(const DiceTestSigninClient&) = delete; + DiceTestSigninClient& operator=(const DiceTestSigninClient&) = delete; + + ~DiceTestSigninClient() override {} + + std::unique_ptr<GaiaAuthFetcher> CreateGaiaAuthFetcher( + GaiaAuthConsumer* consumer, + gaia::GaiaSource source) override { + DCHECK(!consumer_ || (consumer_ == consumer)); + consumer_ = consumer; + + // Pass |this| as a dummy consumer to CreateGaiaAuthFetcher(). + // Since DiceTestSigninClient does not overrides any consumer method, + // everything will be dropped on the floor. + return TestSigninClient::CreateGaiaAuthFetcher(this, source); + } + + // We want to reset |consumer_| here before the test interacts with the last + // consumer. Interacting with the last consumer (simulating success of the + // fetcher) namely sometimes immediately triggers another fetch with another + // consumer. If |consumer_| is non-null, we would hit the DCHECK. + GaiaAuthConsumer* GetAndClearConsumer() { + GaiaAuthConsumer* last_consumer = consumer_; + consumer_ = nullptr; + return last_consumer; + } + + private: + raw_ptr<GaiaAuthConsumer> consumer_; +}; + +class DiceResponseHandlerTest : public testing::Test, + public AccountReconcilor::Observer { + public: + // Called after the refresh token was fetched and added in the token service. + void HandleTokenExchangeSuccess(CoreAccountId account_id, + bool is_new_account) { + token_exchange_account_id_ = account_id; + token_exchange_is_new_account_ = is_new_account; + } + + // Called after the refresh token was fetched and added in the token service. + void EnableSync(const CoreAccountId& account_id) { + enable_sync_account_id_ = account_id; + } + + void HandleTokenExchangeFailure(const std::string& email, + const GoogleServiceAuthError& error) { + auth_error_email_ = email; + auth_error_ = error; + } + + protected: + DiceResponseHandlerTest() + : task_environment_( + base::test::SingleThreadTaskEnvironment::MainThreadType::IO, + base::test::SingleThreadTaskEnvironment::TimeSource:: + MOCK_TIME), // URLRequestContext requires IO. + signin_client_(&pref_service_), + identity_test_env_(/*test_url_loader_factory=*/nullptr, + &pref_service_, + signin::AccountConsistencyMethod::kDice, + &signin_client_), + signin_error_controller_( + SigninErrorController::AccountMode::PRIMARY_ACCOUNT, + identity_test_env_.identity_manager()) { + EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); + AboutSigninInternals::RegisterPrefs(pref_service_.registry()); + auto account_reconcilor_delegate = + std::make_unique<signin::DiceAccountReconcilorDelegate>(); + account_reconcilor_ = std::make_unique<AccountReconcilor>( + identity_test_env_.identity_manager(), &signin_client_, + std::move(account_reconcilor_delegate)); + account_reconcilor_->AddObserver(this); + + about_signin_internals_ = std::make_unique<AboutSigninInternals>( + identity_test_env_.identity_manager(), &signin_error_controller_, + signin::AccountConsistencyMethod::kDice, &signin_client_, + account_reconcilor_.get()); + + dice_response_handler_ = std::make_unique<DiceResponseHandler>( + &signin_client_, identity_test_env_.identity_manager(), + account_reconcilor_.get(), about_signin_internals_.get(), + temp_dir_.GetPath()); + } + + ~DiceResponseHandlerTest() override { + account_reconcilor_->RemoveObserver(this); + account_reconcilor_->Shutdown(); + about_signin_internals_->Shutdown(); + signin_error_controller_.Shutdown(); + } + + DiceResponseParams MakeDiceParams(DiceAction action) { + DiceResponseParams dice_params; + dice_params.user_intention = action; + DiceResponseParams::AccountInfo account_info; + account_info.gaia_id = signin::GetTestGaiaIdForEmail(kEmail); + account_info.email = kEmail; + account_info.session_index = kSessionIndex; + switch (action) { + case DiceAction::SIGNIN: + dice_params.signin_info = + std::make_unique<DiceResponseParams::SigninInfo>(); + dice_params.signin_info->account_info = account_info; + dice_params.signin_info->authorization_code = kAuthorizationCode; + break; + case DiceAction::ENABLE_SYNC: + dice_params.enable_sync_info = + std::make_unique<DiceResponseParams::EnableSyncInfo>(); + dice_params.enable_sync_info->account_info = account_info; + break; + case DiceAction::SIGNOUT: + dice_params.signout_info = + std::make_unique<DiceResponseParams::SignoutInfo>(); + dice_params.signout_info->account_infos.push_back(account_info); + break; + case DiceAction::NONE: + NOTREACHED(); + break; + } + return dice_params; + } + + // AccountReconcilor::Observer: + void OnBlockReconcile() override { ++reconcilor_blocked_count_; } + void OnUnblockReconcile() override { ++reconcilor_unblocked_count_; } + + signin::IdentityManager* identity_manager() { + return identity_test_env_.identity_manager(); + } + + base::test::SingleThreadTaskEnvironment task_environment_; + base::ScopedTempDir temp_dir_; + sync_preferences::TestingPrefServiceSyncable pref_service_; + DiceTestSigninClient signin_client_; + signin::IdentityTestEnvironment identity_test_env_; + SigninErrorController signin_error_controller_; + std::unique_ptr<AboutSigninInternals> about_signin_internals_; + std::unique_ptr<AccountReconcilor> account_reconcilor_; + std::unique_ptr<DiceResponseHandler> dice_response_handler_; + int reconcilor_blocked_count_ = 0; + int reconcilor_unblocked_count_ = 0; + CoreAccountId token_exchange_account_id_; + bool token_exchange_is_new_account_ = false; + CoreAccountId enable_sync_account_id_; + GoogleServiceAuthError auth_error_; + std::string auth_error_email_; +}; + +class TestProcessDiceHeaderDelegate : public ProcessDiceHeaderDelegate { + public: + explicit TestProcessDiceHeaderDelegate(DiceResponseHandlerTest* owner) + : owner_(owner) {} + + ~TestProcessDiceHeaderDelegate() override = default; + + // Called after the refresh token was fetched and added in the token service. + void HandleTokenExchangeSuccess(CoreAccountId account_id, + bool is_new_account) override { + owner_->HandleTokenExchangeSuccess(account_id, is_new_account); + } + + // Called after the refresh token was fetched and added in the token service. + void EnableSync(const CoreAccountId& account_id) override { + owner_->EnableSync(account_id); + } + + void HandleTokenExchangeFailure( + const std::string& email, + const GoogleServiceAuthError& error) override { + owner_->HandleTokenExchangeFailure(email, error); + } + + private: + raw_ptr<DiceResponseHandlerTest> owner_; +}; + +// Checks that a SIGNIN action triggers a token exchange request. +TEST_F(DiceResponseHandlerTest, Signin) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + true /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_TRUE(auth_error_email_.empty()); + EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state()); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id); + EXPECT_TRUE(token_exchange_is_new_account_); + // Check that the reconcilor was blocked and unblocked exactly once. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(1, reconcilor_unblocked_count_); + // Check that the AccountInfo::is_under_advanced_protection is set. + EXPECT_TRUE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id) + .is_under_advanced_protection); +} + +// Checks that the account reconcilor is blocked when where was OAuth +// outage in Dice, and unblocked after the timeout. +TEST_F(DiceResponseHandlerTest, SupportOAuthOutageInDice) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice); + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + dice_params.signin_info->authorization_code.clear(); + dice_params.signin_info->no_authorization_code = true; + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that the reconcilor was blocked and not unblocked before timeout. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + task_environment_.FastForwardBy( + base::Hours(kLockAccountReconcilorTimeoutHours + 1)); + // Check that the reconcilor was unblocked. + EXPECT_EQ(1, reconcilor_unblocked_count_); + EXPECT_EQ(1, reconcilor_blocked_count_); +} + +// Check that after receiving two headers with no authorization code, +// timeout still restarts. +TEST_F(DiceResponseHandlerTest, CheckTimersDuringOutageinDice) { + ASSERT_GT(kLockAccountReconcilorTimeoutHours, 3); + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice); + // Create params for the first header with no authorization code. + DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN); + dice_params_1.signin_info->authorization_code.clear(); + dice_params_1.signin_info->no_authorization_code = true; + dice_response_handler_->ProcessDiceHeader( + dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that the reconcilor was blocked and not unblocked before timeout. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Wait half of the timeout. + task_environment_.FastForwardBy( + base::Hours(kLockAccountReconcilorTimeoutHours / 2)); + // Create params for the second header with no authorization code. + DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN); + dice_params_2.signin_info->authorization_code.clear(); + dice_params_2.signin_info->no_authorization_code = true; + dice_response_handler_->ProcessDiceHeader( + dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + task_environment_.FastForwardBy( + base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2 + 1)); + // Check that the reconcilor was not unblocked after the first timeout + // passed, timer should be restarted after getting the second header. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + task_environment_.FastForwardBy( + base::Hours((kLockAccountReconcilorTimeoutHours + 1) / 2)); + // Check that the reconcilor was unblocked. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(1, reconcilor_unblocked_count_); +} + +// Check that signin works normally (the token is fetched and added to chrome) +// on valid headers after getting a no_authorization_code header. +TEST_F(DiceResponseHandlerTest, CheckSigninAfterOutageInDice) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature(kSupportOAuthOutageInDice); + // Create params for the header with no authorization code. + DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN); + dice_params_1.signin_info->authorization_code.clear(); + dice_params_1.signin_info->no_authorization_code = true; + dice_response_handler_->ProcessDiceHeader( + dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Create params for the valid header with an authorization code. + DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info_2 = dice_params_2.signin_info->account_info; + CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount( + account_info_2.gaia_id, account_info_2.email); + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + dice_response_handler_->ProcessDiceHeader( + dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that the reconcilor was blocked and not unblocked before timeout. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + true /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + EXPECT_TRUE(auth_error_email_.empty()); + EXPECT_EQ(GoogleServiceAuthError::NONE, auth_error_.state()); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id_2); + EXPECT_TRUE(token_exchange_is_new_account_); + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Check that the AccountInfo::is_under_advanced_protection is set. + EXPECT_TRUE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id_2) + .is_under_advanced_protection); + task_environment_.FastForwardBy( + base::Hours(kLockAccountReconcilorTimeoutHours + 1)); + // Check that the reconcilor was unblocked. + EXPECT_EQ(1, reconcilor_unblocked_count_); + EXPECT_EQ(1, reconcilor_blocked_count_); +} + +// Checks that a SIGNIN action triggers a token exchange request when the +// account is in authentication error. +TEST_F(DiceResponseHandlerTest, Reauth) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + dice_params.signin_info->account_info.email, signin::ConsentLevel::kSync); + dice_params.signin_info->account_info.gaia_id = account_info.gaia; + CoreAccountId account_id = account_info.account_id; + identity_test_env_.UpdatePersistentErrorOfRefreshTokenForAccount( + account_id, + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + true /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id)); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id); + EXPECT_FALSE(token_exchange_is_new_account_); +} + +// Checks that a GaiaAuthFetcher failure is handled correctly. +TEST_F(DiceResponseHandlerTest, SigninFailure) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + EXPECT_EQ( + 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Simulate GaiaAuthFetcher failure. + GoogleServiceAuthError::State error_state = + GoogleServiceAuthError::SERVICE_UNAVAILABLE; + consumer->OnClientOAuthFailure(GoogleServiceAuthError(error_state)); + EXPECT_EQ( + 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Check that the token has not been inserted in the token service. + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_EQ(account_info.email, auth_error_email_); + EXPECT_EQ(error_state, auth_error_.state()); +} + +// Checks that a second token for the same account is not requested when a +// request is already in flight. +TEST_F(DiceResponseHandlerTest, SigninRepeatedWithSameAccount) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_1, testing::NotNull()); + // Start a second request for the same account. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that there is no new request. + GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_2, testing::IsNull()); + // Simulate GaiaAuthFetcher success for the first request. + consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + EXPECT_FALSE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id) + .is_under_advanced_protection); +} + +// Checks that two SIGNIN requests can happen concurrently. +TEST_F(DiceResponseHandlerTest, SigninWithTwoAccounts) { + DiceResponseParams dice_params_1 = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info_1 = dice_params_1.signin_info->account_info; + DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN); + dice_params_2.signin_info->account_info.email = "other_email"; + dice_params_2.signin_info->account_info.gaia_id = "other_gaia_id"; + const auto& account_info_2 = dice_params_2.signin_info->account_info; + CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount( + account_info_1.gaia_id, account_info_1.email); + CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount( + account_info_2.gaia_id, account_info_2.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1)); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + // Start first request. + dice_response_handler_->ProcessDiceHeader( + dice_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_1, testing::NotNull()); + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); + // Start second request. + dice_response_handler_->ProcessDiceHeader( + dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_2, testing::NotNull()); + // Simulate GaiaAuthFetcher success for the first request. + consumer_1->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + true /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_1)); + EXPECT_TRUE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id_1) + .is_under_advanced_protection); + // Simulate GaiaAuthFetcher success for the second request. + consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + EXPECT_FALSE(identity_manager() + ->FindExtendedAccountInfoByAccountId(account_id_2) + .is_under_advanced_protection); + // Check that the reconcilor was blocked and unblocked exactly once. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(1, reconcilor_unblocked_count_); +} + +// Checks that a ENABLE_SYNC action received after the refresh token is added +// to the token service, triggers a call to enable sync on the delegate. +TEST_F(DiceResponseHandlerTest, SigninEnableSyncAfterRefreshTokenFetched) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id); + EXPECT_TRUE(token_exchange_is_new_account_); + // Check that delegate was not called to enable sync. + EXPECT_TRUE(enable_sync_account_id_.empty()); + + // Enable sync. + dice_response_handler_->ProcessDiceHeader( + MakeDiceParams(DiceAction::ENABLE_SYNC), + std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that delegate was called to enable sync. + EXPECT_EQ(account_id, enable_sync_account_id_); +} + +// Checks that a ENABLE_SYNC action received before the refresh token is added +// to the token service, is schedules a call to enable sync on the delegate +// once the refresh token is received. +TEST_F(DiceResponseHandlerTest, SigninEnableSyncBeforeRefreshTokenFetched) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + + // Enable sync. + dice_response_handler_->ProcessDiceHeader( + MakeDiceParams(DiceAction::ENABLE_SYNC), + std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that delegate was not called to enable sync. + EXPECT_TRUE(enable_sync_account_id_.empty()); + + // Simulate GaiaAuthFetcher success. + consumer->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + // Check that the token has been inserted in the token service. + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id)); + // Check HandleTokenExchangeSuccess parameters. + EXPECT_EQ(token_exchange_account_id_, account_id); + EXPECT_TRUE(token_exchange_is_new_account_); + // Check that delegate was called to enable sync. + EXPECT_EQ(account_id, enable_sync_account_id_); +} + +TEST_F(DiceResponseHandlerTest, Timeout) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNIN); + const auto& account_info = dice_params.signin_info->account_info; + CoreAccountId account_id = identity_manager()->PickAccountIdForAccount( + account_info.gaia_id, account_info.email); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created. + GaiaAuthConsumer* consumer = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer, testing::NotNull()); + EXPECT_EQ( + 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Force a timeout. + task_environment_.FastForwardBy( + base::Seconds(kDiceTokenFetchTimeoutSeconds + 1)); + EXPECT_EQ( + 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Check that the token has not been inserted in the token service. + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id)); + // Check that the reconcilor was blocked and unblocked exactly once. + EXPECT_EQ(1, reconcilor_blocked_count_); + EXPECT_EQ(1, reconcilor_unblocked_count_); +} + +TEST_F(DiceResponseHandlerTest, SignoutMainAccount) { + const char kSecondaryEmail[] = "other@gmail.com"; + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT); + const auto& dice_account_info = dice_params.signout_info->account_infos[0]; + // User is signed in to Chrome, and has some refresh token for a secondary + // account. + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + dice_account_info.email, signin::ConsentLevel::kSync); + AccountInfo secondary_account_info = + identity_test_env_.MakeAccountAvailable(kSecondaryEmail); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + // Receive signout response for the main account. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + + // User is not signed out, token for the main account is now invalid, + // secondary account is untouched. + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id)); + auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount( + account_info.account_id); + EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state()); + EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason:: + CREDENTIALS_REJECTED_BY_CLIENT, + error.GetInvalidGaiaCredentialsReason()); + + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + secondary_account_info.account_id)); + + EXPECT_TRUE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + // Check that the reconcilor was not blocked. + EXPECT_EQ(0, reconcilor_blocked_count_); + EXPECT_EQ(0, reconcilor_unblocked_count_); +} + +TEST_F(DiceResponseHandlerTest, SignoutSecondaryAccount) { + const char kMainEmail[] = "main@gmail.com"; + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT); + const auto& secondary_dice_account_info = + dice_params.signout_info->account_infos[0]; + // User is signed in to Chrome, and has some refresh token for a secondary + // account. + AccountInfo main_account_info = + identity_test_env_.MakePrimaryAccountAvailable( + kMainEmail, signin::ConsentLevel::kSync); + AccountInfo secondary_account_info = identity_test_env_.MakeAccountAvailable( + secondary_dice_account_info.email); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + main_account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + // Receive signout response for the secondary account. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + + // Only the token corresponding the the Dice parameter has been removed, and + // the user is still signed in. + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + main_account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +TEST_F(DiceResponseHandlerTest, SignoutWebOnly) { + const char kSecondaryEmail[] = "other@gmail.com"; + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT); + const auto& dice_account_info = dice_params.signout_info->account_infos[0]; + // User is NOT signed in to Chrome, and has some refresh tokens for two + // accounts. + AccountInfo account_info = + identity_test_env_.MakeAccountAvailable(dice_account_info.email); + AccountInfo secondary_account_info = + identity_test_env_.MakeAccountAvailable(kSecondaryEmail); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + // Receive signout response. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Only the token corresponding the the Dice parameter has been removed. + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + secondary_account_info.account_id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// Checks that signin in progress is canceled by a signout. +TEST_F(DiceResponseHandlerTest, SigninSignoutSameAccount) { + DiceResponseParams dice_params = MakeDiceParams(DiceAction::SIGNOUT); + const auto& dice_account_info = dice_params.signout_info->account_infos[0]; + + // User is signed in to Chrome. + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + dice_account_info.email, signin::ConsentLevel::kSync); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id)); + // Start Dice signin (reauth). + DiceResponseParams dice_params_2 = MakeDiceParams(DiceAction::SIGNIN); + dice_response_handler_->ProcessDiceHeader( + dice_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that a GaiaAuthFetcher has been created and is pending. + ASSERT_THAT(signin_client_.GetAndClearConsumer(), testing::NotNull()); + EXPECT_EQ( + 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Signout while signin is in flight. + dice_response_handler_->ProcessDiceHeader( + dice_params, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that the token fetcher has been canceled and the token is invalid. + EXPECT_EQ( + 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id)); + auto error = identity_manager()->GetErrorStateOfRefreshTokenForAccount( + account_info.account_id); + EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state()); + EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason:: + CREDENTIALS_REJECTED_BY_CLIENT, + error.GetInvalidGaiaCredentialsReason()); +} + +// Checks that signin in progress is not canceled by a signout for a different +// account. +TEST_F(DiceResponseHandlerTest, SigninSignoutDifferentAccount) { + // User starts signin in the web with two accounts. + DiceResponseParams signout_params_1 = MakeDiceParams(DiceAction::SIGNOUT); + DiceResponseParams signin_params_1 = MakeDiceParams(DiceAction::SIGNIN); + DiceResponseParams signin_params_2 = MakeDiceParams(DiceAction::SIGNIN); + signin_params_2.signin_info->account_info.email = "other_email"; + signin_params_2.signin_info->account_info.gaia_id = "other_gaia_id"; + const auto& signin_account_info_1 = signin_params_1.signin_info->account_info; + const auto& signin_account_info_2 = signin_params_2.signin_info->account_info; + CoreAccountId account_id_1 = identity_manager()->PickAccountIdForAccount( + signin_account_info_1.gaia_id, signin_account_info_1.email); + CoreAccountId account_id_2 = identity_manager()->PickAccountIdForAccount( + signin_account_info_2.gaia_id, signin_account_info_2.email); + dice_response_handler_->ProcessDiceHeader( + signin_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + + GaiaAuthConsumer* consumer_1 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_1, testing::NotNull()); + dice_response_handler_->ProcessDiceHeader( + signin_params_2, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + GaiaAuthConsumer* consumer_2 = signin_client_.GetAndClearConsumer(); + ASSERT_THAT(consumer_2, testing::NotNull()); + EXPECT_EQ( + 2u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1)); + ASSERT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + ASSERT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id_1)); + ASSERT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id_2)); + // Signout from one of the accounts while signin is in flight. + dice_response_handler_->ProcessDiceHeader( + signout_params_1, std::make_unique<TestProcessDiceHeaderDelegate>(this)); + // Check that one of the fetchers is cancelled. + EXPECT_EQ( + 1u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Allow the remaining fetcher to complete. + consumer_2->OnClientOAuthSuccess(GaiaAuthConsumer::ClientOAuthResult( + "refresh_token", "access_token", 10, false /* is_child_account */, + false /* is_advanced_protection*/)); + EXPECT_EQ( + 0u, dice_response_handler_->GetPendingDiceTokenFetchersCountForTesting()); + // Check that the right token is available. + EXPECT_FALSE(identity_manager()->HasAccountWithRefreshToken(account_id_1)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_id_2)); + EXPECT_FALSE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + account_id_2)); +} + +// Tests that the DiceResponseHandler is created for a normal profile but not +// for off-the-record profiles. +TEST(DiceResponseHandlerFactoryTest, NotInOffTheRecord) { + content::BrowserTaskEnvironment task_environment; + TestingProfile profile; + EXPECT_THAT(DiceResponseHandler::GetForProfile(&profile), testing::NotNull()); + EXPECT_THAT(DiceResponseHandler::GetForProfile( + profile.GetPrimaryOTRProfile(/*create_if_needed=*/true)), + testing::IsNull()); + EXPECT_THAT(DiceResponseHandler::GetForProfile(profile.GetOffTheRecordProfile( + Profile::OTRProfileID::CreateUniqueForTesting(), + /*create_if_needed=*/true)), + testing::IsNull()); +} + +} // namespace diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc new file mode 100644 index 00000000000..494a17b4695 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.cc @@ -0,0 +1,211 @@ +// 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. + +#include "chrome/browser/signin/dice_signed_in_profile_creator.h" + +#include <string> + +#include "base/check.h" +#include "base/location.h" +#include "base/memory/ptr_util.h" +#include "base/memory/raw_ptr.h" +#include "base/scoped_observation.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_avatar_icon_util.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +// Waits until the tokens are loaded and calls the callback. The callback is +// called immediately if the tokens are already loaded, and called with nullptr +// if the profile is destroyed before the tokens are loaded. +class TokensLoadedCallbackRunner : public signin::IdentityManager::Observer { + public: + ~TokensLoadedCallbackRunner() override = default; + TokensLoadedCallbackRunner(const TokensLoadedCallbackRunner&) = delete; + TokensLoadedCallbackRunner& operator=(const TokensLoadedCallbackRunner&) = + delete; + + // Runs the callback when the tokens are loaded. If tokens are already loaded + // the callback is called synchronously and this returns nullptr. + static std::unique_ptr<TokensLoadedCallbackRunner> RunWhenLoaded( + Profile* profile, + base::OnceCallback<void(Profile*)> callback); + + private: + TokensLoadedCallbackRunner(Profile* profile, + base::OnceCallback<void(Profile*)> callback); + + // signin::IdentityManager::Observer implementation: + void OnRefreshTokensLoaded() override { + scoped_identity_manager_observer_.Reset(); + std::move(callback_).Run(profile_.get()); + } + + void OnIdentityManagerShutdown(signin::IdentityManager* manager) override { + scoped_identity_manager_observer_.Reset(); + std::move(callback_).Run(nullptr); + } + + raw_ptr<Profile> profile_; + raw_ptr<signin::IdentityManager> identity_manager_; + base::ScopedObservation<signin::IdentityManager, + signin::IdentityManager::Observer> + scoped_identity_manager_observer_{this}; + base::OnceCallback<void(Profile*)> callback_; +}; + +// static +std::unique_ptr<TokensLoadedCallbackRunner> +TokensLoadedCallbackRunner::RunWhenLoaded( + Profile* profile, + base::OnceCallback<void(Profile*)> callback) { + if (IdentityManagerFactory::GetForProfile(profile) + ->AreRefreshTokensLoaded()) { + std::move(callback).Run(profile); + return nullptr; + } + + return base::WrapUnique( + new TokensLoadedCallbackRunner(profile, std::move(callback))); +} + +TokensLoadedCallbackRunner::TokensLoadedCallbackRunner( + Profile* profile, + base::OnceCallback<void(Profile*)> callback) + : profile_(profile), + identity_manager_(IdentityManagerFactory::GetForProfile(profile)), + callback_(std::move(callback)) { + DCHECK(profile_); + DCHECK(identity_manager_); + DCHECK(callback_); + DCHECK(!identity_manager_->AreRefreshTokensLoaded()); + scoped_identity_manager_observer_.Observe(identity_manager_.get()); +} + +DiceSignedInProfileCreator::DiceSignedInProfileCreator( + Profile* source_profile, + CoreAccountId account_id, + const std::u16string& local_profile_name, + absl::optional<size_t> icon_index, + bool use_guest_profile, + base::OnceCallback<void(Profile*)> callback) + : source_profile_(source_profile), + account_id_(account_id), + callback_(std::move(callback)) { + // Passing the sign-in token to an ephemeral Guest profile is part of the + // experiment to surface a Guest mode link in the DiceWebSigninIntercept + // and is only used to sign in to the web through account consistency and + // does NOT enable sync or any other browser level functionality. + // TODO(https://crbug.com/1225171): Revise the comment after Guest mode plans + // are finalized. + if (use_guest_profile) { + // TODO(https://crbug.com/1225171): Re-enabled if ephemeral based Guest mode + // is added. Remove the code otherwise. + NOTREACHED(); + + // Make sure the callback is not called synchronously. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&ProfileManager::CreateProfileAsync, + base::Unretained(g_browser_process->profile_manager()), + ProfileManager::GetGuestProfilePath(), + base::BindRepeating( + &DiceSignedInProfileCreator::OnNewProfileCreated, + weak_pointer_factory_.GetWeakPtr()))); + } else { + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + if (!icon_index.has_value()) + icon_index = storage.ChooseAvatarIconIndexForNewProfile(); + std::u16string name = local_profile_name.empty() + ? storage.ChooseNameForNewProfile(*icon_index) + : local_profile_name; + ProfileManager::CreateMultiProfileAsync( + name, *icon_index, /*is_hidden=*/false, + base::BindRepeating(&DiceSignedInProfileCreator::OnNewProfileCreated, + weak_pointer_factory_.GetWeakPtr())); + } +} + +DiceSignedInProfileCreator::DiceSignedInProfileCreator( + Profile* source_profile, + CoreAccountId account_id, + const base::FilePath& target_profile_path, + base::OnceCallback<void(Profile*)> callback) + : source_profile_(source_profile), + account_id_(account_id), + callback_(std::move(callback)) { + // Make sure the callback is not called synchronously. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce( + base::IgnoreResult(&ProfileManager::LoadProfileByPath), + base::Unretained(g_browser_process->profile_manager()), + target_profile_path, /*incognito=*/false, + base::BindOnce(&DiceSignedInProfileCreator::OnNewProfileInitialized, + weak_pointer_factory_.GetWeakPtr()))); +} + +DiceSignedInProfileCreator::~DiceSignedInProfileCreator() = default; + +void DiceSignedInProfileCreator::OnNewProfileCreated( + Profile* new_profile, + Profile::CreateStatus status) { + switch (status) { + case Profile::CREATE_STATUS_CREATED: + // Ignore this, wait for profile to be initialized. + return; + case Profile::CREATE_STATUS_INITIALIZED: + OnNewProfileInitialized(new_profile); + return; + case Profile::CREATE_STATUS_LOCAL_FAIL: + NOTREACHED() << "Error creating new profile"; + if (callback_) + std::move(callback_).Run(nullptr); + return; + } +} + +void DiceSignedInProfileCreator::OnNewProfileInitialized(Profile* new_profile) { + if (!new_profile) { + if (callback_) + std::move(callback_).Run(nullptr); + return; + } + + DCHECK(!tokens_loaded_callback_runner_); + // base::Unretained is fine because the runner is owned by this. + auto tokens_loaded_callback_runner = + TokensLoadedCallbackRunner::RunWhenLoaded( + new_profile, + base::BindOnce(&DiceSignedInProfileCreator::OnNewProfileTokensLoaded, + base::Unretained(this))); + // If the callback was called synchronously, |this| may have been deleted. + if (tokens_loaded_callback_runner) { + tokens_loaded_callback_runner_ = std::move(tokens_loaded_callback_runner); + } +} + +void DiceSignedInProfileCreator::OnNewProfileTokensLoaded( + Profile* new_profile) { + tokens_loaded_callback_runner_.reset(); + if (!new_profile) { + if (callback_) + std::move(callback_).Run(nullptr); + return; + } + + auto* accounts_mutator = + IdentityManagerFactory::GetForProfile(source_profile_) + ->GetAccountsMutator(); + auto* new_profile_accounts_mutator = + IdentityManagerFactory::GetForProfile(new_profile)->GetAccountsMutator(); + accounts_mutator->MoveAccount(new_profile_accounts_mutator, account_id_); + if (callback_) + std::move(callback_).Run(new_profile); +} diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h new file mode 100644 index 00000000000..97bb462da96 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator.h @@ -0,0 +1,70 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_ +#define CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_ + +#include <string> + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "chrome/browser/profiles/profile.h" +#include "google_apis/gaia/core_account_id.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +class TokensLoadedCallbackRunner; + +// Extracts an account from an existing profile and moves it to a new profile. +class DiceSignedInProfileCreator { + public: + // Creates a new profile or uses Guest profile if |use_guest_profile|, and + // moves the account from source_profile to it. + // The callback is called with the new profile or nullptr in case of failure. + // The callback is never called synchronously. + // If |local_profile_name| is not empty, it will be set as local name for the + // new profile. + // If |icon_index| is nullopt, a random icon will be selected. + DiceSignedInProfileCreator(Profile* source_profile, + CoreAccountId account_id, + const std::u16string& local_profile_name, + absl::optional<size_t> icon_index, + bool use_guest_profile, + base::OnceCallback<void(Profile*)> callback); + + // Uses this version when the profile already exists at `target_profile_path` + // but may not be loaded in memory. The profile is loaded if necessary, and + // the account is moved. + DiceSignedInProfileCreator(Profile* source_profile, + CoreAccountId account_id, + const base::FilePath& target_profile_path, + base::OnceCallback<void(Profile*)> callback); + + ~DiceSignedInProfileCreator(); + + DiceSignedInProfileCreator(const DiceSignedInProfileCreator&) = delete; + DiceSignedInProfileCreator& operator=(const DiceSignedInProfileCreator&) = + delete; + + private: + // Callback invoked once a profile is created, so we can transfer the + // credentials. + void OnNewProfileCreated(Profile* new_profile, Profile::CreateStatus status); + + // Called when the profile is initialized. + void OnNewProfileInitialized(Profile* new_profile); + + // Callback invoked once the token service is ready for the new profile. + void OnNewProfileTokensLoaded(Profile* new_profile); + + const raw_ptr<Profile> source_profile_; + const CoreAccountId account_id_; + + base::OnceCallback<void(Profile*)> callback_; + std::unique_ptr<TokensLoadedCallbackRunner> tokens_loaded_callback_runner_; + + base::WeakPtrFactory<DiceSignedInProfileCreator> weak_pointer_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_SIGNED_IN_PROFILE_CREATOR_H_ diff --git a/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc b/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc new file mode 100644 index 00000000000..b0d813a17ec --- /dev/null +++ b/chromium/chrome/browser/signin/dice_signed_in_profile_creator_unittest.cc @@ -0,0 +1,285 @@ +// 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. + +#include "chrome/browser/signin/dice_signed_in_profile_creator.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_avatar_icon_util.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_manager_observer.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/test/base/fake_profile_manager.h" +#include "chrome/test/base/scoped_testing_local_state.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char16_t kProfileTestName[] = u"profile_test_name"; + +std::unique_ptr<TestingProfile> BuildTestingProfile(const base::FilePath& path, + Profile::Delegate* delegate, + bool tokens_loaded) { + TestingProfile::Builder profile_builder; + profile_builder.SetDelegate(delegate); + profile_builder.SetPath(path); + std::unique_ptr<TestingProfile> profile = + IdentityTestEnvironmentProfileAdaptor:: + CreateProfileForIdentityTestEnvironment(profile_builder); + if (!tokens_loaded) { + IdentityTestEnvironmentProfileAdaptor adaptor(profile.get()); + adaptor.identity_test_env()->ResetToAccountsNotYetLoadedFromDiskState(); + } + if (profile->GetPath() == ProfileManager::GetGuestProfilePath()) + profile->SetGuestSession(true); + return profile; +} + +class UnittestProfileManager : public FakeProfileManager { + public: + explicit UnittestProfileManager(const base::FilePath& user_data_dir) + : FakeProfileManager(user_data_dir) {} + + void set_tokens_loaded_at_creation(bool loaded) { + tokens_loaded_at_creation_ = loaded; + } + + std::unique_ptr<TestingProfile> BuildTestingProfile( + const base::FilePath& path, + Profile::Delegate* delegate) override { + return ::BuildTestingProfile(path, delegate, tokens_loaded_at_creation_); + } + + bool tokens_loaded_at_creation_ = true; +}; + +} // namespace + +class DiceSignedInProfileCreatorTest : public testing::Test, + public ProfileManagerObserver { + public: + DiceSignedInProfileCreatorTest() + : local_state_(TestingBrowserProcess::GetGlobal()) { + EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); + auto profile_manager_unique = + std::make_unique<UnittestProfileManager>(temp_dir_.GetPath()); + profile_manager_ = profile_manager_unique.get(); + TestingBrowserProcess::GetGlobal()->SetProfileManager( + std::move(profile_manager_unique)); + profile_ = BuildTestingProfile(base::FilePath(), /*delegate=*/nullptr, + /*tokens_loaded=*/true); + identity_test_env_profile_adaptor_ = + std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile()); + profile_manager()->AddObserver(this); + } + + ~DiceSignedInProfileCreatorTest() override { DeleteProfiles(); } + + UnittestProfileManager* profile_manager() { return profile_manager_; } + + // Test environment attached to profile(). + signin::IdentityTestEnvironment* identity_test_env() { + return identity_test_env_profile_adaptor_->identity_test_env(); + } + + // Source profile (the one which we are extracting credentials from). + Profile* profile() { return profile_.get(); } + + // Profile created by the DiceSignedInProfileCreator. + Profile* signed_in_profile() { return signed_in_profile_; } + + // Profile added to the ProfileManager. In general this should be the same as + // signed_in_profile() except in error cases. + Profile* added_profile() { return added_profile_; } + + bool creator_callback_called() { return creator_callback_called_; } + + void set_profile_added_closure(base::OnceClosure closure) { + profile_added_closure_ = std::move(closure); + } + + bool use_guest_profile() const { return use_guest_profile_; } + + void DeleteProfiles() { + identity_test_env_profile_adaptor_.reset(); + if (profile_manager_) { + profile_manager()->RemoveObserver(this); + TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr); + profile_manager_ = nullptr; + } + } + + // Callback for the DiceSignedInProfileCreator. + void OnProfileCreated(base::OnceClosure quit_closure, Profile* profile) { + creator_callback_called_ = true; + signed_in_profile_ = profile; + if (quit_closure) + std::move(quit_closure).Run(); + } + + // ProfileManagerObserver: + void OnProfileAdded(Profile* profile) override { + added_profile_ = profile; + if (profile_added_closure_) + std::move(profile_added_closure_).Run(); + } + + private: + content::BrowserTaskEnvironment task_environment_; + base::ScopedTempDir temp_dir_; + ScopedTestingLocalState local_state_; + raw_ptr<UnittestProfileManager> profile_manager_ = nullptr; + std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> + identity_test_env_profile_adaptor_; + std::unique_ptr<TestingProfile> profile_; + raw_ptr<Profile> signed_in_profile_ = nullptr; + raw_ptr<Profile> added_profile_ = nullptr; + base::OnceClosure profile_added_closure_; + bool creator_callback_called_ = false; + base::test::ScopedFeatureList scoped_feature_list_; + bool use_guest_profile_ = false; +}; + +TEST_F(DiceSignedInProfileCreatorTest, CreateWithTokensLoaded) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + size_t kTestIcon = profiles::GetModernAvatarIconStartIndex(); + + base::RunLoop loop; + std::unique_ptr<DiceSignedInProfileCreator> creator = + std::make_unique<DiceSignedInProfileCreator>( + profile(), account_info.account_id, kProfileTestName, kTestIcon, + use_guest_profile(), + base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated, + base::Unretained(this), loop.QuitClosure())); + loop.Run(); + + // Check that the account was moved. + EXPECT_TRUE(creator_callback_called()); + EXPECT_TRUE(signed_in_profile()); + EXPECT_NE(profile(), signed_in_profile()); + EXPECT_EQ(signed_in_profile(), added_profile()); + EXPECT_FALSE(IdentityManagerFactory::GetForProfile(profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_EQ(1u, IdentityManagerFactory::GetForProfile(signed_in_profile()) + ->GetAccountsWithRefreshTokens() + .size()); + EXPECT_TRUE(IdentityManagerFactory::GetForProfile(signed_in_profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + + // Check profile type + ASSERT_EQ(use_guest_profile(), signed_in_profile()->IsGuestSession()); + + // Check the profile name and icon. + ProfileAttributesStorage& storage = + profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(signed_in_profile()->GetPath()); + ASSERT_TRUE(entry); + if (!use_guest_profile()) { + EXPECT_EQ(kProfileTestName, entry->GetLocalProfileName()); + EXPECT_EQ(kTestIcon, entry->GetAvatarIconIndex()); + } +} + +TEST_F(DiceSignedInProfileCreatorTest, CreateWithTokensNotLoaded) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + profile_manager()->set_tokens_loaded_at_creation(false); + + base::RunLoop creator_loop; + base::RunLoop profile_added_loop; + set_profile_added_closure(profile_added_loop.QuitClosure()); + std::unique_ptr<DiceSignedInProfileCreator> creator = + std::make_unique<DiceSignedInProfileCreator>( + profile(), account_info.account_id, std::u16string(), absl::nullopt, + use_guest_profile(), + base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated, + base::Unretained(this), creator_loop.QuitClosure())); + profile_added_loop.Run(); + base::RunLoop().RunUntilIdle(); + + // The profile was created, but tokens not loaded. The callback has not been + // called yet. + EXPECT_FALSE(creator_callback_called()); + EXPECT_TRUE(added_profile()); + EXPECT_NE(profile(), added_profile()); + + // Load the tokens. + IdentityTestEnvironmentProfileAdaptor adaptor(added_profile()); + adaptor.identity_test_env()->ReloadAccountsFromDisk(); + creator_loop.Run(); + + // Check that the account was moved. + EXPECT_EQ(signed_in_profile(), added_profile()); + EXPECT_TRUE(creator_callback_called()); + EXPECT_FALSE(IdentityManagerFactory::GetForProfile(profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + EXPECT_EQ(1u, IdentityManagerFactory::GetForProfile(signed_in_profile()) + ->GetAccountsWithRefreshTokens() + .size()); + EXPECT_TRUE(IdentityManagerFactory::GetForProfile(signed_in_profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); +} + +// Deleting the creator while it is running does not crash. +TEST_F(DiceSignedInProfileCreatorTest, DeleteWhileCreating) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + std::unique_ptr<DiceSignedInProfileCreator> creator = + std::make_unique<DiceSignedInProfileCreator>( + profile(), account_info.account_id, std::u16string(), absl::nullopt, + use_guest_profile(), + base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated, + base::Unretained(this), base::OnceClosure())); + EXPECT_FALSE(creator_callback_called()); + creator.reset(); + base::RunLoop().RunUntilIdle(); +} + +// Deleting the profile while waiting for the tokens. +TEST_F(DiceSignedInProfileCreatorTest, DeleteProfile) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + profile_manager()->set_tokens_loaded_at_creation(false); + + base::RunLoop creator_loop; + base::RunLoop profile_added_loop; + set_profile_added_closure(profile_added_loop.QuitClosure()); + std::unique_ptr<DiceSignedInProfileCreator> creator = + std::make_unique<DiceSignedInProfileCreator>( + profile(), account_info.account_id, std::u16string(), absl::nullopt, + use_guest_profile(), + base::BindOnce(&DiceSignedInProfileCreatorTest::OnProfileCreated, + base::Unretained(this), creator_loop.QuitClosure())); + profile_added_loop.Run(); + base::RunLoop().RunUntilIdle(); + + // The profile was created, but tokens not loaded. The callback has not been + // called yet. + EXPECT_FALSE(creator_callback_called()); + EXPECT_TRUE(added_profile()); + EXPECT_NE(profile(), added_profile()); + + DeleteProfiles(); + creator_loop.Run(); + + // The callback is called with nullptr profile. + EXPECT_TRUE(creator_callback_called()); + EXPECT_FALSE(signed_in_profile()); +} diff --git a/chromium/chrome/browser/signin/dice_tab_helper.cc b/chromium/chrome/browser/signin/dice_tab_helper.cc new file mode 100644 index 00000000000..4e538d31a98 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_tab_helper.cc @@ -0,0 +1,117 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/dice_tab_helper.h" + +#include "base/check_op.h" +#include "base/metrics/user_metrics.h" +#include "chrome/browser/signin/dice_tab_helper.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/ui/browser_finder.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/browser/navigation_handle.h" +#include "google_apis/gaia/gaia_urls.h" + +DiceTabHelper::DiceTabHelper(content::WebContents* web_contents) + : content::WebContentsUserData<DiceTabHelper>(*web_contents), + content::WebContentsObserver(web_contents) {} + +DiceTabHelper::~DiceTabHelper() = default; + +void DiceTabHelper::InitializeSigninFlow( + const GURL& signin_url, + signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + signin_metrics::PromoAction promo_action, + const GURL& redirect_url) { + DCHECK(signin_url.is_valid()); + DCHECK(signin_url_.is_empty() || signin_url_ == signin_url); + + signin_url_ = signin_url; + signin_access_point_ = access_point; + signin_reason_ = reason; + signin_promo_action_ = promo_action; + is_chrome_signin_page_ = true; + signin_page_load_recorded_ = false; + redirect_url_ = redirect_url; + sync_signin_flow_status_ = SyncSigninFlowStatus::kNotStarted; + + if (reason == signin_metrics::Reason::kSigninPrimaryAccount) { + sync_signin_flow_status_ = SyncSigninFlowStatus::kStarted; + signin_metrics::LogSigninAccessPointStarted(access_point, promo_action); + signin_metrics::RecordSigninUserActionForAccessPoint(access_point, + promo_action); + base::RecordAction(base::UserMetricsAction("Signin_SigninPage_Loading")); + } +} + +bool DiceTabHelper::IsChromeSigninPage() const { + return is_chrome_signin_page_; +} + +bool DiceTabHelper::IsSyncSigninInProgress() const { + return sync_signin_flow_status_ == SyncSigninFlowStatus::kStarted; +} + +void DiceTabHelper::OnSyncSigninFlowComplete() { + // The flow is complete, reset to initial state. + sync_signin_flow_status_ = SyncSigninFlowStatus::kNotStarted; +} + +void DiceTabHelper::DidStartNavigation( + content::NavigationHandle* navigation_handle) { + if (!is_chrome_signin_page_) + return; + + // Ignore internal navigations. + if (!navigation_handle->IsInPrimaryMainFrame() || + navigation_handle->IsSameDocument()) { + return; + } + + if (!IsSigninPageNavigation(navigation_handle)) { + // Navigating away from the signin page. + // Note that currently any indication of a navigation is enough to consider + // this tab unsuitable for re-use, even if the navigation does not end up + // committing. + is_chrome_signin_page_ = false; + } +} + +void DiceTabHelper::DidFinishNavigation( + content::NavigationHandle* navigation_handle) { + if (!is_chrome_signin_page_) + return; + + // Ignore internal navigations. + if (!navigation_handle->IsInPrimaryMainFrame() || + navigation_handle->IsSameDocument()) { + return; + } + + if (!IsSigninPageNavigation(navigation_handle)) { + // Navigating away from the signin page. + // Note that currently any indication of a navigation is enough to consider + // this tab unsuitable for re-use, even if the navigation does not end up + // committing. + is_chrome_signin_page_ = false; + return; + } + + if (!signin_page_load_recorded_) { + signin_page_load_recorded_ = true; + base::RecordAction(base::UserMetricsAction("Signin_SigninPage_Shown")); + } +} + +bool DiceTabHelper::IsSigninPageNavigation( + content::NavigationHandle* navigation_handle) const { + return !navigation_handle->IsErrorPage() && + navigation_handle->GetRedirectChain()[0] == signin_url_ && + navigation_handle->GetURL().DeprecatedGetOriginAsURL() == + GaiaUrls::GetInstance()->gaia_url(); +} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(DiceTabHelper); diff --git a/chromium/chrome/browser/signin/dice_tab_helper.h b/chromium/chrome/browser/signin/dice_tab_helper.h new file mode 100644 index 00000000000..2f6190a4f98 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_tab_helper.h @@ -0,0 +1,97 @@ +// Copyright 2017 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. + +#ifndef CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_ + +#include "components/signin/public/base/signin_metrics.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" + +namespace content { +class NavigationHandle; +} + +// Tab helper used for DICE to tag signin tabs. Signin tabs can be reused. +class DiceTabHelper : public content::WebContentsUserData<DiceTabHelper>, + public content::WebContentsObserver { + public: + DiceTabHelper(const DiceTabHelper&) = delete; + DiceTabHelper& operator=(const DiceTabHelper&) = delete; + + ~DiceTabHelper() override; + + signin_metrics::AccessPoint signin_access_point() const { + return signin_access_point_; + } + + signin_metrics::PromoAction signin_promo_action() const { + return signin_promo_action_; + } + + signin_metrics::Reason signin_reason() const { return signin_reason_; } + + const GURL& redirect_url() const { return redirect_url_; } + + const GURL& signin_url() const { return signin_url_; } + + // Initializes the DiceTabHelper for a new signin flow. Must be called once + // per signin flow happening in the tab, when the signin URL is being loaded. + void InitializeSigninFlow(const GURL& signin_url, + signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + signin_metrics::PromoAction promo_action, + const GURL& redirect_url); + + // Returns true if this the tab is a re-usable chrome sign-in page (the signin + // page is loading or loaded in the tab). + // Returns false if the user or the page has navigated away from |signin_url|. + bool IsChromeSigninPage() const; + + // Returns true if a signin flow was initialized with the reason + // kSigninPrimaryAccount and is not yet complete. + // Note that there is not guarantee that the flow would ever finish, and in + // some rare cases it is possible that a "non-sync" signin happens while this + // is true (if the user aborts the flow and then re-uses the same tab for a + // normal web signin). + bool IsSyncSigninInProgress() const; + + // Called to notify that the sync signin is complete. + void OnSyncSigninFlowComplete(); + + private: + friend class content::WebContentsUserData<DiceTabHelper>; + explicit DiceTabHelper(content::WebContents* web_contents); + + // kStarted: a Sync signin flow was started and not completed. + // kNotStarted: there is no sync signin flow in progress. + enum class SyncSigninFlowStatus { kNotStarted, kStarted }; + + // content::WebContentsObserver: + void DidStartNavigation( + content::NavigationHandle* navigation_handle) override; + void DidFinishNavigation( + content::NavigationHandle* navigation_handle) override; + + // Returns true if this is a navigation to the signin URL. + bool IsSigninPageNavigation( + content::NavigationHandle* navigation_handle) const; + + GURL redirect_url_; + GURL signin_url_; + signin_metrics::AccessPoint signin_access_point_ = + signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN; + signin_metrics::PromoAction signin_promo_action_ = + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO; + signin_metrics::Reason signin_reason_ = + signin_metrics::Reason::kUnknownReason; + bool is_chrome_signin_page_ = false; + bool signin_page_load_recorded_ = false; + SyncSigninFlowStatus sync_signin_flow_status_ = + SyncSigninFlowStatus::kNotStarted; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_TAB_HELPER_H_ diff --git a/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc b/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc new file mode 100644 index 00000000000..96eab075f2a --- /dev/null +++ b/chromium/chrome/browser/signin/dice_tab_helper_unittest.cc @@ -0,0 +1,253 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/dice_tab_helper.h" + +#include "base/test/metrics/histogram_tester.h" +#include "base/test/metrics/user_action_tester.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "chrome/test/base/testing_profile.h" +#include "components/signin/public/base/signin_metrics.h" +#include "content/public/common/content_features.h" +#include "content/public/test/back_forward_cache_util.h" +#include "content/public/test/navigation_simulator.h" +#include "content/public/test/test_renderer_host.h" +#include "content/public/test/web_contents_tester.h" +#include "google_apis/gaia/gaia_urls.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/features.h" + +class DiceTabHelperTest : public ChromeRenderViewHostTestHarness { + public: + DiceTabHelperTest() { + signin_url_ = GaiaUrls::GetInstance()->signin_chrome_sync_dice(); + feature_list_.InitWithFeaturesAndParameters( + {{features::kBackForwardCache, {}}, + {features::kBackForwardCacheMemoryControls, {}}}, + {}); + } + + // Does a navigation to Gaia and initializes the tab helper. + void InitializeDiceTabHelper(DiceTabHelper* helper, + signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason) { + // Load the signin page. + std::unique_ptr<content::NavigationSimulator> simulator = + content::NavigationSimulator::CreateRendererInitiated(signin_url_, + main_rfh()); + simulator->Start(); + helper->InitializeSigninFlow( + signin_url_, access_point, reason, + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO, + GURL::EmptyGURL()); + EXPECT_TRUE(helper->IsChromeSigninPage()); + simulator->Commit(); + } + + GURL signin_url_; + base::test::ScopedFeatureList feature_list_; +}; + +// Tests DiceTabHelper intialization. +TEST_F(DiceTabHelperTest, Initialization) { + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + + // Check default state. + EXPECT_EQ(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN, + dice_tab_helper->signin_access_point()); + EXPECT_EQ(signin_metrics::Reason::kUnknownReason, + dice_tab_helper->signin_reason()); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + + // Initialize the signin flow. + signin_metrics::AccessPoint access_point = + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE; + signin_metrics::Reason reason = signin_metrics::Reason::kSigninPrimaryAccount; + InitializeDiceTabHelper(dice_tab_helper, access_point, reason); + EXPECT_EQ(access_point, dice_tab_helper->signin_access_point()); + EXPECT_EQ(reason, dice_tab_helper->signin_reason()); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); +} + +TEST_F(DiceTabHelperTest, SigninPageStatus) { + // The test assumes the previous page gets deleted after navigation and will + // be recreated after navigation (which resets the signin page state). Disable + // back/forward cache to ensure that it doesn't get preserved in the cache. + content::DisableBackForwardCacheForTesting( + web_contents(), content::BackForwardCache::TEST_ASSUMES_NO_CACHING); + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + + // Load the signin page. + signin_metrics::AccessPoint access_point = + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE; + signin_metrics::Reason reason = signin_metrics::Reason::kSigninPrimaryAccount; + InitializeDiceTabHelper(dice_tab_helper, access_point, reason); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + + // Reloading the signin page does not interrupt the signin flow. + content::NavigationSimulator::Reload(web_contents()); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + + // Subframe navigation are ignored. + std::unique_ptr<content::NavigationSimulator> simulator = + content::NavigationSimulator::CreateRendererInitiated( + signin_url_.Resolve("#baz"), main_rfh()); + simulator->CommitSameDocument(); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + + // Navigation in subframe does not interrupt the signin flow. + content::RenderFrameHostTester* render_frame_host_tester = + content::RenderFrameHostTester::For(main_rfh()); + content::RenderFrameHost* sub_frame = + render_frame_host_tester->AppendChild("subframe"); + content::NavigationSimulator::NavigateAndCommitFromDocument(signin_url_, + sub_frame); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + + // Navigating to a different page resets the page status. + simulator = content::NavigationSimulator::CreateRendererInitiated( + signin_url_.Resolve("/foo"), main_rfh()); + simulator->Start(); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + simulator->Commit(); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + + // Go Back to the signin page + content::NavigationSimulator::GoBack(web_contents()); + // IsChromeSigninPage() returns false after navigating away from the + // signin page. + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + + // Navigate away from the signin page + content::NavigationSimulator::GoForward(web_contents()); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); +} + +// Tests DiceTabHelper metrics. +TEST_F(DiceTabHelperTest, Metrics) { + base::UserActionTester ua_tester; + base::HistogramTester h_tester; + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + + // No metrics are logged when the Dice tab helper is created. + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_Signin_FromStartPage")); + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Check metrics logged when the Dice tab helper is initialized. + std::unique_ptr<content::NavigationSimulator> simulator = + content::NavigationSimulator::CreateRendererInitiated(signin_url_, + main_rfh()); + simulator->Start(); + dice_tab_helper->InitializeSigninFlow( + signin_url_, signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, + signin_metrics::Reason::kSigninPrimaryAccount, + signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT, + GURL::EmptyGURL()); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_Signin_FromSettings")); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + h_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint", + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1); + h_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1); + + // First call to did finish load does logs any Signin_SigninPage_Shown user + // action. + simulator->Commit(); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Second call to did finish load does not log any metrics. + dice_tab_helper->DidFinishLoad(main_rfh(), signin_url_); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Check metrics are logged again when the Dice tab helper is re-initialized. + simulator = content::NavigationSimulator::CreateRendererInitiated(signin_url_, + main_rfh()); + simulator->Start(); + dice_tab_helper->InitializeSigninFlow( + signin_url_, signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, + signin_metrics::Reason::kSigninPrimaryAccount, + signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT, + GURL::EmptyGURL()); + EXPECT_EQ(2, ua_tester.GetActionCount("Signin_Signin_FromSettings")); + EXPECT_EQ(2, ua_tester.GetActionCount("Signin_SigninPage_Loading")); + h_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint", + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 2); + h_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.WithDefault", + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, 1); +} + +TEST_F(DiceTabHelperTest, IsSyncSigninInProgress) { + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); + + // Non-sync signin. + InitializeDiceTabHelper(dice_tab_helper, + signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS, + signin_metrics::Reason::kAddSecondaryAccount); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); + + // Sync signin + InitializeDiceTabHelper(dice_tab_helper, + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, + signin_metrics::Reason::kSigninPrimaryAccount); + EXPECT_TRUE(dice_tab_helper->IsSyncSigninInProgress()); + dice_tab_helper->OnSyncSigninFlowComplete(); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); +} + +class DiceTabHelperPrerenderTest : public DiceTabHelperTest { + public: + DiceTabHelperPrerenderTest() { + feature_list_.InitWithFeatures( + {blink::features::kPrerender2}, + // Disable the memory requirement of Prerender2 so the test can run on + // any bot. + {blink::features::kPrerender2MemoryControls}); + } + + ~DiceTabHelperPrerenderTest() override = default; + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(DiceTabHelperPrerenderTest, SigninStatusAfterPrerendering) { + base::UserActionTester ua_tester; + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + EXPECT_FALSE(dice_tab_helper->IsChromeSigninPage()); + EXPECT_EQ(0, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Sync signin + InitializeDiceTabHelper(dice_tab_helper, + signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS, + signin_metrics::Reason::kSigninPrimaryAccount); + dice_tab_helper->OnSyncSigninFlowComplete(); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown")); + + // Starting prerendering a page doesn't navigate away from the signin page. + content::WebContentsTester::For(web_contents()) + ->AddPrerenderAndCommitNavigation(signin_url_.Resolve("/foo/test.html")); + EXPECT_TRUE(dice_tab_helper->IsChromeSigninPage()); + EXPECT_EQ(1, ua_tester.GetActionCount("Signin_SigninPage_Shown")); +} diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc new file mode 100644 index 00000000000..751c3a58fad --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.cc @@ -0,0 +1,851 @@ +// 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. + +#include "chrome/browser/signin/dice_web_signin_interceptor.h" + +#include <string> + +#include "base/check.h" +#include "base/hash/hash.h" +#include "base/i18n/case_conversion.h" +#include "base/metrics/field_trial_params.h" +#include "base/metrics/histogram_functions.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/enterprise/util/managed_browser_utils.h" +#include "chrome/browser/net/system_network_context_manager.h" +#include "chrome/browser/new_tab_page/chrome_colors/generated_colors_info.h" +#include "chrome/browser/password_manager/chrome_password_manager_client.h" +#include "chrome/browser/policy/chrome_browser_policy_connector.h" +#include "chrome/browser/policy/profile_policy_connector.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_avatar_icon_util.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_metrics.h" +#include "chrome/browser/profiles/profiles_state.h" +#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h" +#include "chrome/browser/signin/dice_signed_in_profile_creator.h" +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/themes/theme_service.h" +#include "chrome/browser/themes/theme_service_factory.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h" +#include "chrome/browser/ui/signin/profile_colors_util.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper_delegate_impl.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/themes/autogenerated_theme_util.h" +#include "components/password_manager/core/browser/password_manager.h" +#include "components/password_manager/core/common/password_manager_ui.h" +#include "components/policy/core/browser/browser_policy_connector.h" +#include "components/policy/core/browser/signin/user_cloud_signin_restriction_policy_fetcher.h" +#include "components/policy/core/common/features.h" +#include "components/policy/core/common/policy_map.h" +#include "components/policy/core/common/policy_namespace.h" +#include "components/policy/core/common/policy_service.h" +#include "components/policy/policy_constants.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "ui/base/l10n/l10n_util.h" + +namespace { + +constexpr char kProfileCreationInterceptionDeclinedPref[] = + "signin.ProfileCreationInterceptionDeclinedPref"; + +void RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome outcome) { + base::UmaHistogramEnumeration("Signin.Intercept.HeuristicOutcome", outcome); +} + +// Helper function to return the primary account info. The returned info is +// empty if there is no primary account, and non-empty otherwise. Extended +// fields may be missing if they are not available. +AccountInfo GetPrimaryAccountInfo(signin::IdentityManager* manager) { + CoreAccountInfo primary_core_account_info = + manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + if (primary_core_account_info.IsEmpty()) + return AccountInfo(); + + AccountInfo primary_account_info = + manager->FindExtendedAccountInfo(primary_core_account_info); + + if (!primary_account_info.IsEmpty()) + return primary_account_info; + + // Return an AccountInfo without extended fields, based on the core info. + AccountInfo account_info; + account_info.gaia = primary_core_account_info.gaia; + account_info.email = primary_core_account_info.email; + account_info.account_id = primary_core_account_info.account_id; + return account_info; +} + +bool HasNoBrowser(content::WebContents* web_contents) { + return chrome::FindBrowserWithWebContents(web_contents) == nullptr; +} + +// Returns true if enterprise separation is required. +// Returns false is enterprise separation is not required. +// Returns no value if info is required to determine if enterprise separation is +// required. +// If `managed_account_profile_level_signin_restriction` is `absl::nullopt` then +// the user cloud policy value of ManagedAccountsSigninRestriction has not yet +// been fetched. If it is an empty string, then the value has been fetched but +// no policy was set. +absl::optional<bool> EnterpriseSeparationMaybeRequired( + Profile* profile, + const std::string& email, + signin::IdentityManager* identity_manager, + bool is_new_account_interception, + absl::optional<std::string> + managed_account_profile_level_signin_restriction) { + // No enterprise separation required if the feature is disabled. + if (!base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)) + return false; + // No enterprise separation required for consumer accounts. + if (policy::BrowserPolicyConnector::IsNonEnterpriseUser(email)) + return false; + + auto intercepted_account_info = + identity_manager->FindExtendedAccountInfoByEmailAddress(email); + // If the account info is not found, we need to wait for the info to be + // available. + if (!intercepted_account_info.IsValid()) + return absl::nullopt; + // If the intercepted account is not managed, no interception required. + if (!intercepted_account_info.IsManaged()) + return false; + // If `profile` requires enterprise profile separation, return true. + if (signin_util::ProfileSeparationEnforcedByPolicy( + profile, managed_account_profile_level_signin_restriction.value_or( + std::string()))) { + return true; + } + // If we still do not know if profile separation is required, the account + // level policies for the intercepted account must be fetched if possible. + if (is_new_account_interception && + base::FeatureList::IsEnabled( + policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher) && + !managed_account_profile_level_signin_restriction.has_value() && + g_browser_process->system_network_context_manager()) { + return absl::nullopt; + } + + return false; +} + +} // namespace + +ScopedDiceWebSigninInterceptionBubbleHandle:: + ~ScopedDiceWebSigninInterceptionBubbleHandle() = default; + +bool SigninInterceptionHeuristicOutcomeIsSuccess( + SigninInterceptionHeuristicOutcome outcome) { + return outcome == SigninInterceptionHeuristicOutcome::kInterceptEnterprise || + outcome == SigninInterceptionHeuristicOutcome::kInterceptMultiUser || + outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch; +} + +DiceWebSigninInterceptor::DiceWebSigninInterceptor( + Profile* profile, + std::unique_ptr<Delegate> delegate) + : profile_(profile), + identity_manager_(IdentityManagerFactory::GetForProfile(profile)), + delegate_(std::move(delegate)) { + DCHECK(profile_); + DCHECK(identity_manager_); + DCHECK(delegate_); +} + +DiceWebSigninInterceptor::~DiceWebSigninInterceptor() = default; + +// static +void DiceWebSigninInterceptor::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterDictionaryPref(kProfileCreationInterceptionDeclinedPref); + registry->RegisterBooleanPref(prefs::kSigninInterceptionEnabled, true); + registry->RegisterStringPref(prefs::kManagedAccountsSigninRestriction, + std::string()); + registry->RegisterBooleanPref( + prefs::kManagedAccountsSigninRestrictionScopeMachine, false); +} + +absl::optional<SigninInterceptionHeuristicOutcome> +DiceWebSigninInterceptor::GetHeuristicOutcome( + bool is_new_account, + bool is_sync_signin, + const std::string& email, + const ProfileAttributesEntry** entry) const { + if (!profile_->GetPrefs()->GetBoolean(prefs::kSigninInterceptionEnabled)) + return SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled; + + if (is_sync_signin) { + // Do not intercept signins from the Sync startup flow. + // Note: |is_sync_signin| is an approximation, and in rare cases it may be + // true when in fact the signin was not a sync signin. In this case the + // interception is missed. + return SigninInterceptionHeuristicOutcome::kAbortSyncSignin; + } + // Wait for more account info is enterprise separation is required or if more + // info is needed. + if (EnterpriseSeparationMaybeRequired( + profile_, email, identity_manager_, is_new_account, + /*managed_account_profile_level_signin_restriction=*/absl::nullopt) + .value_or(true)) { + return absl::nullopt; + } + + if (!is_new_account) { + // Do not intercept reauth. + return SigninInterceptionHeuristicOutcome::kAbortAccountNotNew; + } + + const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble( + email, + &g_browser_process->profile_manager()->GetProfileAttributesStorage()); + if (switch_to_entry) { + if (entry) + *entry = switch_to_entry; + return SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch; + } + + // From this point the remaining possible interceptions involve creating a new + // profile. + if (!profiles::IsProfileCreationAllowed()) { + return SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed; + } + + std::vector<CoreAccountInfo> accounts_in_chrome = + identity_manager_->GetAccountsWithRefreshTokens(); + if (accounts_in_chrome.size() == 0 || + (accounts_in_chrome.size() == 1 && + gaia::AreEmailsSame(email, accounts_in_chrome[0].email))) { + // Enterprise and multi-user bubbles are only shown if there are multiple + // accounts. The intercepted account may not be added to chrome yet. + return SigninInterceptionHeuristicOutcome::kAbortSingleAccount; + } + + if (HasUserDeclinedProfileCreation(email)) { + return SigninInterceptionHeuristicOutcome:: + kAbortUserDeclinedProfileForAccount; + } + + return absl::nullopt; +} + +void DiceWebSigninInterceptor::MaybeInterceptWebSignin( + content::WebContents* web_contents, + CoreAccountId account_id, + bool is_new_account, + bool is_sync_signin) { + if (is_interception_in_progress_) { + // Multiple concurrent interceptions are not supported. + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress); + return; + } + + if (!web_contents) { + // The tab has been closed (typically during the token exchange, which may + // take some time). + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortTabClosed); + return; + } + + if (HasNoBrowser(web_contents)) { + // Do not intercept from the profile creation flow. + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortNoBrowser); + return; + } + + // Do not show the interception UI if a password update is required: both + // bubbles cannot be shown at the same time and the password update is more + // important. + ChromePasswordManagerClient* password_manager_client = + ChromePasswordManagerClient::FromWebContents(web_contents); + if (password_manager_client && password_manager_client->GetPasswordManager() + ->IsFormManagerPendingPasswordUpdate()) { + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortPasswordUpdatePending); + return; + } + + ManagePasswordsUIController* password_controller = + ManagePasswordsUIController::FromWebContents(web_contents); + if (password_controller && + password_controller->GetState() == + password_manager::ui::State::PENDING_PASSWORD_UPDATE_STATE) { + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortPasswordUpdate); + return; + } + + AccountInfo account_info = + identity_manager_->FindExtendedAccountInfoByAccountId(account_id); + DCHECK(!account_info.IsEmpty()) << "Intercepting unknown account."; + const ProfileAttributesEntry* entry = nullptr; + absl::optional<SigninInterceptionHeuristicOutcome> heuristic_outcome = + GetHeuristicOutcome(is_new_account, is_sync_signin, account_info.email, + &entry); + account_id_ = account_id; + is_interception_in_progress_ = true; + new_account_interception_ = is_new_account; + web_contents_ = web_contents->GetWeakPtr(); + + if (heuristic_outcome) { + RecordSigninInterceptionHeuristicOutcome(*heuristic_outcome); + if (*heuristic_outcome == + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch) { + DCHECK(entry); + Delegate::BubbleParameters bubble_parameters{ + SigninInterceptionType::kProfileSwitch, account_info, + GetPrimaryAccountInfo(identity_manager_), + entry->GetProfileThemeColors().profile_highlight_color, + /*show_guest_option=*/false}; + interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble( + web_contents, bubble_parameters, + base::BindOnce(&DiceWebSigninInterceptor::OnProfileSwitchChoice, + base::Unretained(this), account_info.email, + entry->GetPath())); + was_interception_ui_displayed_ = true; + } else { + // Interception is aborted. + DCHECK(!SigninInterceptionHeuristicOutcomeIsSuccess(*heuristic_outcome)); + Reset(); + } + return; + } + + account_info_fetch_start_time_ = base::TimeTicks::Now(); + if (account_info.IsValid()) { + OnExtendedAccountInfoUpdated(account_info); + } else { + on_account_info_update_timeout_.Reset(base::BindOnce( + &DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout, + base::Unretained(this))); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, on_account_info_update_timeout_.callback(), + base::Seconds(5)); + account_info_update_observation_.Observe(identity_manager_.get()); + } +} + +void DiceWebSigninInterceptor::CreateBrowserAfterSigninInterception( + CoreAccountId account_id, + content::WebContents* intercepted_contents, + std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> bubble_handle, + bool is_new_profile) { + DCHECK(!session_startup_helper_); + DCHECK(bubble_handle); + interception_bubble_handle_ = std::move(bubble_handle); + session_startup_helper_ = + std::make_unique<DiceInterceptedSessionStartupHelper>( + profile_, is_new_profile, account_id, intercepted_contents); + session_startup_helper_->Startup( + base::BindOnce(&DiceWebSigninInterceptor::OnNewBrowserCreated, + base::Unretained(this), is_new_profile)); +} + +void DiceWebSigninInterceptor::Shutdown() { + if (is_interception_in_progress_ && !was_interception_ui_displayed_) { + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortShutdown); + } + Reset(); +} + +void DiceWebSigninInterceptor::Reset() { + web_contents_ = nullptr; + account_info_update_observation_.Reset(); + on_account_info_update_timeout_.Cancel(); + is_interception_in_progress_ = false; + account_id_ = CoreAccountId(); + new_account_interception_ = false; + intercepted_account_management_accepted_ = false; + dice_signed_in_profile_creator_.reset(); + was_interception_ui_displayed_ = false; + account_info_fetch_start_time_ = base::TimeTicks(); + profile_creation_start_time_ = base::TimeTicks(); + interception_bubble_handle_.reset(); + on_intercepted_account_level_policy_value_timeout_.Cancel(); + account_level_signin_restriction_policy_fetcher_.reset(); + intercepted_account_level_policy_value_.reset(); +} + +const ProfileAttributesEntry* +DiceWebSigninInterceptor::ShouldShowProfileSwitchBubble( + const std::string& intercepted_email, + ProfileAttributesStorage* profile_attribute_storage) const { + // Check if there is already an existing profile with this account. + base::FilePath profile_path = profile_->GetPath(); + for (const auto* entry : + profile_attribute_storage->GetAllProfilesAttributes()) { + if (entry->GetPath() == profile_path) + continue; + if (gaia::AreEmailsSame(intercepted_email, + base::UTF16ToUTF8(entry->GetUserName()))) { + return entry; + } + } + return nullptr; +} + +bool DiceWebSigninInterceptor::ShouldEnforceEnterpriseProfileSeparation( + const AccountInfo& intercepted_account_info) const { + DCHECK(intercepted_account_info.IsValid()); + + if (!signin_util::ProfileSeparationEnforcedByPolicy( + profile_, + intercepted_account_level_policy_value_.value_or(std::string()))) { + return false; + } + if (new_account_interception_) + return intercepted_account_info.IsManaged(); + + CoreAccountInfo primary_core_account_info = + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + // In case of re-auth, do not show the enterprise separation dialog if the + // user already consented to enterprise management. + if (!new_account_interception_ && primary_core_account_info.account_id == + intercepted_account_info.account_id) { + return !chrome::enterprise_util::UserAcceptedAccountManagement(profile_); + } + + return false; +} + +bool DiceWebSigninInterceptor::ShouldShowEnterpriseBubble( + const AccountInfo& intercepted_account_info) const { + DCHECK(intercepted_account_info.IsValid()); + // Check if the intercepted account or the primary account is managed. + CoreAccountInfo primary_core_account_info = + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + + if (primary_core_account_info.IsEmpty() || + primary_core_account_info.account_id == + intercepted_account_info.account_id) { + return false; + } + + if (intercepted_account_info.IsManaged()) + return true; + + return identity_manager_->FindExtendedAccountInfo(primary_core_account_info) + .IsManaged(); +} + +bool DiceWebSigninInterceptor::ShouldShowMultiUserBubble( + const AccountInfo& intercepted_account_info) const { + DCHECK(intercepted_account_info.IsValid()); + if (identity_manager_->GetAccountsWithRefreshTokens().size() <= 1u) + return false; + // Check if the account has the same name as another account in the profile. + for (const auto& account_info : + identity_manager_->GetExtendedAccountInfoForAccountsWithRefreshToken()) { + if (account_info.account_id == intercepted_account_info.account_id) + continue; + // Case-insensitve comparison supporting non-ASCII characters. + if (base::i18n::FoldCase(base::UTF8ToUTF16(account_info.given_name)) == + base::i18n::FoldCase( + base::UTF8ToUTF16(intercepted_account_info.given_name))) { + return false; + } + } + return true; +} + +void DiceWebSigninInterceptor::OnInterceptionReadyToBeProcessed( + const AccountInfo& info) { + DCHECK_EQ(info.account_id, account_id_); + DCHECK(info.IsValid()); + + absl::optional<SigninInterceptionType> interception_type; + + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile_->GetPath()); + SkColor profile_color = GenerateNewProfileColor(entry).color; + + const ProfileAttributesEntry* switch_to_entry = ShouldShowProfileSwitchBubble( + info.email, + &g_browser_process->profile_manager()->GetProfileAttributesStorage()); + + bool force_profile_separation = + ShouldEnforceEnterpriseProfileSeparation(info); + +#if DCHECK_IS_ON() + if (force_profile_separation) { + DCHECK(base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync) || + !profile_->GetPrefs() + ->GetString(prefs::kManagedAccountsSigninRestriction) + .empty()); + } +#endif + + if (switch_to_entry) { + // Propose account switching if we skipped in GetHeuristicOutcome because we + // returned a nullptr to get more information about forced enterprise + // profile separation. + interception_type = force_profile_separation + ? SigninInterceptionType::kProfileSwitchForced + : SigninInterceptionType::kProfileSwitch; + RecordSigninInterceptionHeuristicOutcome( + force_profile_separation + ? SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch + : SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); + } else if (force_profile_separation) { + // In case of a reauth of an account that already had sync enabled, + // the user already accepted to use a managed profile. Simply update that + // fact. + if (!new_account_interception_ && + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync) == + info.account_id) { + chrome::enterprise_util::SetUserAcceptedAccountManagement(profile_, true); + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortAccountNotNew); + Reset(); + return; + } + interception_type = SigninInterceptionType::kEnterpriseForced; + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); + } else if (ShouldShowEnterpriseBubble(info)) { + interception_type = SigninInterceptionType::kEnterprise; + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kInterceptEnterprise); + } else if (ShouldShowMultiUserBubble(info)) { + interception_type = SigninInterceptionType::kMultiUser; + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kInterceptMultiUser); + } + + if (!interception_type) { + // Signin should not be intercepted. + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortAccountInfoNotCompatible); + Reset(); + return; + } + + Delegate::BubbleParameters bubble_parameters{ + *interception_type, info, GetPrimaryAccountInfo(identity_manager_), + GetAutogeneratedThemeColors(profile_color).frame_color, + /*show_guest_option=*/false}; + + base::OnceCallback<void(SigninInterceptionResult)> callback; + switch (*interception_type) { + case SigninInterceptionType::kProfileSwitchForced: + callback = base::BindOnce( + &DiceWebSigninInterceptor::OnProfileSwitchChoice, + base::Unretained(this), info.email, switch_to_entry->GetPath()); + break; + case SigninInterceptionType::kEnterpriseForced: + callback = base::BindOnce( + &DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult, + base::Unretained(this), info, profile_color); + break; + case SigninInterceptionType::kProfileSwitch: + case SigninInterceptionType::kEnterprise: + case SigninInterceptionType::kMultiUser: + callback = + base::BindOnce(&DiceWebSigninInterceptor::OnProfileCreationChoice, + base::Unretained(this), info, profile_color); + break; + } + interception_bubble_handle_ = delegate_->ShowSigninInterceptionBubble( + web_contents_.get(), bubble_parameters, std::move(callback)); + + was_interception_ui_displayed_ = true; +} + +void DiceWebSigninInterceptor::OnExtendedAccountInfoUpdated( + const AccountInfo& info) { + if (info.account_id != account_id_) + return; + if (!info.IsValid()) + return; + + account_info_update_observation_.Reset(); + on_account_info_update_timeout_.Cancel(); + base::UmaHistogramTimes( + "Signin.Intercept.AccountInfoFetchDuration", + base::TimeTicks::Now() - account_info_fetch_start_time_); + + // Fetch the ManagedAccountsSigninRestriction policy value for the intercepted + // account with a timeout. + if (!EnterpriseSeparationMaybeRequired( + profile_, info.email, identity_manager_, new_account_interception_, + intercepted_account_level_policy_value_) + .has_value()) { + FetchAccountLevelSigninRestrictionForInterceptedAccount( + info, base::BindOnce( + &DiceWebSigninInterceptor:: + OnAccountLevelManagedAccountsSigninRestrictionReceived, + base::Unretained(this), /*timed_out=*/false, info)); + return; + } + + OnInterceptionReadyToBeProcessed(info); +} + +void DiceWebSigninInterceptor::OnExtendedAccountInfoFetchTimeout() { + RecordSigninInterceptionHeuristicOutcome( + SigninInterceptionHeuristicOutcome::kAbortAccountInfoTimeout); + Reset(); +} + +void DiceWebSigninInterceptor::OnProfileCreationChoice( + const AccountInfo& account_info, + SkColor profile_color, + SigninInterceptionResult create) { + if (create != SigninInterceptionResult::kAccepted && + create != SigninInterceptionResult::kAcceptedWithGuest) { + if (create == SigninInterceptionResult::kDeclined) + RecordProfileCreationDeclined(account_info.email); + Reset(); + return; + } + + DCHECK(interception_bubble_handle_); + profile_creation_start_time_ = base::TimeTicks::Now(); + std::u16string profile_name; + profile_name = profiles::GetDefaultNameForNewSignedInProfile(account_info); + + DCHECK(!dice_signed_in_profile_creator_); + // Unretained is fine because the profile creator is owned by this. + dice_signed_in_profile_creator_ = + std::make_unique<DiceSignedInProfileCreator>( + profile_, account_id_, profile_name, + profiles::GetPlaceholderAvatarIndex(), + create == SigninInterceptionResult::kAcceptedWithGuest, + base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated, + base::Unretained(this), profile_color)); +} + +void DiceWebSigninInterceptor::OnProfileSwitchChoice( + const std::string& email, + const base::FilePath& profile_path, + SigninInterceptionResult switch_profile) { + if (switch_profile != SigninInterceptionResult::kAccepted) { + Reset(); + return; + } + + DCHECK(interception_bubble_handle_); + DCHECK(!dice_signed_in_profile_creator_); + profile_creation_start_time_ = base::TimeTicks::Now(); + // Unretained is fine because the profile creator is owned by this. + dice_signed_in_profile_creator_ = + std::make_unique<DiceSignedInProfileCreator>( + profile_, account_id_, profile_path, + base::BindOnce(&DiceWebSigninInterceptor::OnNewSignedInProfileCreated, + base::Unretained(this), absl::nullopt)); +} + +void DiceWebSigninInterceptor::OnNewSignedInProfileCreated( + absl::optional<SkColor> profile_color, + Profile* new_profile) { + DCHECK(dice_signed_in_profile_creator_); + dice_signed_in_profile_creator_.reset(); + + if (!new_profile) { + Reset(); + return; + } + + // The profile color is defined only when the profile has just been created + // (with interception type kMultiUser or kEnterprise). If the profile is not + // new (kProfileSwitch) or if it is a guest profile, then the color is not + // updated. + bool is_new_profile = profile_color.has_value(); + if (is_new_profile) { + base::UmaHistogramTimes( + "Signin.Intercept.ProfileCreationDuration", + base::TimeTicks::Now() - profile_creation_start_time_); + ProfileMetrics::LogProfileAddNewUser( + ProfileMetrics::ADD_NEW_USER_SIGNIN_INTERCEPTION); + // TODO(https://crbug.com/1225171): Remove the condition if Guest mode + // option is removed. + if (!new_profile->IsGuestSession()) { + // Apply the new color to the profile. + ThemeServiceFactory::GetForProfile(new_profile) + ->BuildAutogeneratedThemeFromColor(*profile_color); + } + } else { + base::UmaHistogramTimes( + "Signin.Intercept.ProfileSwitchDuration", + base::TimeTicks::Now() - profile_creation_start_time_); + } + + if (base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)) { + chrome::enterprise_util::SetUserAcceptedAccountManagement( + new_profile, intercepted_account_management_accepted_); + } + + // Work is done in this profile, the flow continues in the + // DiceWebSigninInterceptor that is attached to the new profile. + DiceWebSigninInterceptorFactory::GetForProfile(new_profile) + ->CreateBrowserAfterSigninInterception( + account_id_, web_contents_.get(), + std::move(interception_bubble_handle_), is_new_profile); + Reset(); +} + +void DiceWebSigninInterceptor::OnEnterpriseProfileCreationResult( + const AccountInfo& account_info, + SkColor profile_color, + SigninInterceptionResult create) { + DCHECK(base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)); + if (create == SigninInterceptionResult::kAccepted) { + intercepted_account_management_accepted_ = true; + // In case of a reauth if there was no consent for management, do not create + // a new profile. + if (!new_account_interception_ && + GetPrimaryAccountInfo(identity_manager_).account_id == + account_info.account_id) { + chrome::enterprise_util::SetUserAcceptedAccountManagement( + profile_, intercepted_account_management_accepted_); + Reset(); + } else { + OnProfileCreationChoice(account_info, profile_color, + SigninInterceptionResult::kAccepted); + } + } else { + DCHECK_EQ(SigninInterceptionResult::kDeclined, create) + << "The user can only accept or decline"; + OnProfileCreationChoice(account_info, profile_color, + SigninInterceptionResult::kDeclined); + auto* accounts_mutator = identity_manager_->GetAccountsMutator(); + accounts_mutator->RemoveAccount( + account_info.account_id, + signin_metrics::SourceForRefreshTokenOperation:: + kDiceTurnOnSyncHelper_Abort); + } + signin_util::RecordEnterpriseProfileCreationUserChoice( + /*enforced_by_policy=*/signin_util::ProfileSeparationEnforcedByPolicy( + profile_, + intercepted_account_level_policy_value_.value_or(std::string())), + /*created=*/create == SigninInterceptionResult::kAccepted); +} + +void DiceWebSigninInterceptor::OnNewBrowserCreated(bool is_new_profile) { + DCHECK(interception_bubble_handle_); + interception_bubble_handle_.reset(); // Close the bubble now. + session_startup_helper_.reset(); + + // TODO(https://crbug.com/1225171): Remove |IsGuestSession| if Guest option is + // no more supported. + if (!is_new_profile || profile_->IsGuestSession()) + return; + + // Don't show the customization bubble if a valid policy theme is set. + Browser* browser = chrome::FindBrowserWithProfile(profile_); + if (ThemeServiceFactory::GetForProfile(profile_)->UsingPolicyTheme()) { + // Show the profile switch IPH that is normally shown after the + // customization bubble. + browser->window()->MaybeShowProfileSwitchIPH(); + return; + } + + DCHECK(browser); + delegate_->ShowProfileCustomizationBubble(browser); +} + +// static +std::string DiceWebSigninInterceptor::GetPersistentEmailHash( + const std::string& email) { + int hash = base::PersistentHash( + gaia::CanonicalizeEmail(gaia::SanitizeEmail(email))) & + 0xFF; + return base::StringPrintf("email_%i", hash); +} + +void DiceWebSigninInterceptor::RecordProfileCreationDeclined( + const std::string& email) { + DictionaryPrefUpdate update(profile_->GetPrefs(), + kProfileCreationInterceptionDeclinedPref); + std::string key = GetPersistentEmailHash(email); + absl::optional<int> declined_count = update->FindIntKey(key); + update->SetIntKey(key, declined_count.value_or(0) + 1); +} + +bool DiceWebSigninInterceptor::HasUserDeclinedProfileCreation( + const std::string& email) const { + const base::DictionaryValue* pref_data = profile_->GetPrefs()->GetDictionary( + kProfileCreationInterceptionDeclinedPref); + absl::optional<int> declined_count = + pref_data->FindIntKey(GetPersistentEmailHash(email)); + // Check if the user declined 2 times. + constexpr int kMaxProfileCreationDeclinedCount = 2; + return declined_count && + declined_count.value() >= kMaxProfileCreationDeclinedCount; +} + +void DiceWebSigninInterceptor:: + FetchAccountLevelSigninRestrictionForInterceptedAccount( + const AccountInfo& account_info, + base::OnceCallback<void(const std::string&)> callback) { + DCHECK(base::FeatureList::IsEnabled( + policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher)); + if (intercepted_account_level_policy_value_fetch_result_for_testing_ + .has_value()) { + std::move(callback).Run( + intercepted_account_level_policy_value_fetch_result_for_testing_ + .value()); + return; + } + + account_level_signin_restriction_policy_fetcher_ = + std::make_unique<policy::UserCloudSigninRestrictionPolicyFetcher>( + g_browser_process->browser_policy_connector(), + g_browser_process->system_network_context_manager() + ->GetSharedURLLoaderFactory()); + account_level_signin_restriction_policy_fetcher_ + ->GetManagedAccountsSigninRestriction( + identity_manager_, account_info.account_id, std::move(callback)); + + on_intercepted_account_level_policy_value_timeout_.Reset(base::BindOnce( + &DiceWebSigninInterceptor:: + OnAccountLevelManagedAccountsSigninRestrictionReceived, + base::Unretained(this), /*timed_out=*/true, account_info, std::string())); + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, on_intercepted_account_level_policy_value_timeout_.callback(), + base::Seconds(5)); +} + +void DiceWebSigninInterceptor:: + OnAccountLevelManagedAccountsSigninRestrictionReceived( + bool timed_out, + const AccountInfo& account_info, + const std::string& signin_restriction) { +#if DCHECK_IS_ON() + if (timed_out) { + DCHECK(signin_restriction.empty()) + << "There should be no signin restriction at the account level in case " + "of a timeout"; + } +#endif + intercepted_account_level_policy_value_ = signin_restriction; + OnInterceptionReadyToBeProcessed(account_info); +} diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h new file mode 100644 index 00000000000..0e3a03f41c3 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor.h @@ -0,0 +1,411 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_ +#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_ + +#include <memory> + +#include "base/callback_forward.h" +#include "base/cancelable_callback.h" +#include "base/feature_list.h" +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "base/scoped_observation.h" +#include "base/time/time.h" +#include "chrome/browser/ui/webui/signin/enterprise_profile_welcome_ui.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "google_apis/gaia/core_account_id.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/skia/include/core/SkColor.h" + +namespace base { +class FilePath; +} + +namespace content { +class WebContents; +} + +namespace policy { +class UserCloudSigninRestrictionPolicyFetcher; +} +namespace user_prefs { +class PrefRegistrySyncable; +} + +struct AccountInfo; +class Browser; +class DiceSignedInProfileCreator; +class DiceInterceptedSessionStartupHelper; +class Profile; +class ProfileAttributesEntry; +class ProfileAttributesStorage; + +// Outcome of the interception heuristic (decision whether the interception +// bubble is shown or not). +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class SigninInterceptionHeuristicOutcome { + // Interception succeeded: + kInterceptProfileSwitch = 0, + kInterceptMultiUser = 1, + kInterceptEnterprise = 2, + + // Interception aborted: + // This is a "Sync" sign in and not a "web" sign in. + kAbortSyncSignin = 3, + // Another interception is already in progress. + kAbortInterceptInProgress = 4, + // This is not a new account (reauth). + kAbortAccountNotNew = 5, + // New profile is not offered when there is only one account. + kAbortSingleAccount = 6, + // Extended account info could not be downloaded. + kAbortAccountInfoTimeout = 7, + // Account info not compatible with interception (e.g. same Gaia name). + kAbortAccountInfoNotCompatible = 8, + // Profile creation disallowed. + kAbortProfileCreationDisallowed = 9, + // The interceptor was shut down before the heuristic completed. + kAbortShutdown = 10, + // The interceptor is not offered when WebContents has no browser associated. + kAbortNoBrowser = 11, + // A password update is required for the account, and this takes priority over + // signin interception. + kAbortPasswordUpdate = 12, + // A password update will be required for the account: the password used on + // the form does not match the stored password. + kAbortPasswordUpdatePending = 13, + // The user already declined a new profile for this account, the UI is not + // shown again. + kAbortUserDeclinedProfileForAccount = 14, + // Signin interception is disabled by the SigninInterceptionEnabled policy. + kAbortInterceptionDisabled = 15, + + // Interception succeeded when enteprise account separation is mandatory. + kInterceptEnterpriseForced = 16, + kInterceptEnterpriseForcedProfileSwitch = 17, + + // The interceptor is not triggered if the tab has already been closed. + kAbortTabClosed = 18, + + kMaxValue = kAbortTabClosed, +}; + +// User selection in the interception bubble. +enum class SigninInterceptionUserChoice { kAccept, kDecline, kGuest }; + +// User action resulting from the interception bubble. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class SigninInterceptionResult { + kAccepted = 0, + kDeclined = 1, + kIgnored = 2, + + // Used when the bubble was not shown because it's not implemented. + kNotDisplayed = 3, + + // Accepted to be opened in Guest profile. + kAcceptedWithGuest = 4, + + kMaxValue = kAcceptedWithGuest, +}; + +// The ScopedDiceWebSigninInterceptionBubbleHandle closes the signin intercept +// bubble when it is destroyed, if the bubble is still opened. Note that this +// handle does not prevent the bubble from being closed for other reasons. +class ScopedDiceWebSigninInterceptionBubbleHandle { + public: + virtual ~ScopedDiceWebSigninInterceptionBubbleHandle() = 0; +}; + +// Returns whether the heuristic outcome is a success (the signin should be +// intercepted). +bool SigninInterceptionHeuristicOutcomeIsSuccess( + SigninInterceptionHeuristicOutcome outcome); + +// Called after web signed in, after a successful token exchange through Dice. +// The DiceWebSigninInterceptor may offer the user to create a new profile or +// switch to another existing profile. +// +// Implementation notes: here is how an entire interception flow work for the +// enterprise or multi-user case: +// * MaybeInterceptWebSignin() is called when the new signin happens. +// * Wait until the account info is downloaded. +// * Interception UI is shown by the delegate. Keep a handle on the bubble. +// * If the user approved, a new profile is created and the token is moved from +// this profile to the new profile, using DiceSignedInProfileCreator. +// * At this point, the flow ends in this profile, and continues in the new +// profile using DiceInterceptedSessionStartupHelper to add the account. +// * When the account is available on the web in the new profile: +// - A new browser window is created for the new profile, +// - The tab is moved to the new profile, +// - The interception bubble is closed by deleting the handle, +// - The profile customization bubble is shown. +class DiceWebSigninInterceptor : public KeyedService, + public signin::IdentityManager::Observer { + public: + enum class SigninInterceptionType { + kProfileSwitch, + kEnterprise, + kMultiUser, + kEnterpriseForced, + kProfileSwitchForced + }; + + // Delegate class responsible for showing the various interception UIs. + class Delegate { + public: + // Parameters for interception bubble UIs. + struct BubbleParameters { + SigninInterceptionType interception_type; + AccountInfo intercepted_account; + AccountInfo primary_account; + SkColor profile_highlight_color; + bool show_guest_option; + }; + + virtual ~Delegate() = default; + + // Shows the signin interception bubble and calls |callback| to indicate + // whether the user should continue in a new profile. + // The callback is never called if the delegate is deleted before it + // completes. + // May return a nullptr handle if the bubble cannot be shown. + // Warning: the handle closes the bubble when it is destroyed ; it is the + // responsibility of the caller to keep the handle alive until the bubble + // should be closed. + // The callback must not be called synchronously if this function returns a + // valid handle (because the caller needs to be able to close the bubble + // from the callback). + virtual std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> + ShowSigninInterceptionBubble( + content::WebContents* web_contents, + const BubbleParameters& bubble_parameters, + base::OnceCallback<void(SigninInterceptionResult)> callback) = 0; + + // Shows the profile customization bubble. + virtual void ShowProfileCustomizationBubble(Browser* browser) = 0; + }; + + DiceWebSigninInterceptor(Profile* profile, + std::unique_ptr<Delegate> delegate); + ~DiceWebSigninInterceptor() override; + + DiceWebSigninInterceptor(const DiceWebSigninInterceptor&) = delete; + DiceWebSigninInterceptor& operator=(const DiceWebSigninInterceptor&) = delete; + + static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + + // Called when an account has been added in Chrome from the web (using the + // DICE protocol). + // |web_contents| is the tab where the signin event happened. It must belong + // to the profile associated with this service. It may be nullptr if the tab + // was closed. + // |is_new_account| is true if the account was not already in Chrome (i.e. + // this is not a reauth). + // |is_sync_signin| is true if the user is signing in with the intent of + // enabling sync for that account. + // Virtual for testing. + virtual void MaybeInterceptWebSignin(content::WebContents* web_contents, + CoreAccountId account_id, + bool is_new_account, + bool is_sync_signin); + + // Called after the new profile was created during a signin interception. + // The token has been moved to the new profile, but the account is not yet in + // the cookies. + // `intercepted_contents` may be null if the tab was already closed. + // The intercepted web contents belong to the source profile (which is not the + // profile attached to this service). + void CreateBrowserAfterSigninInterception( + CoreAccountId account_id, + content::WebContents* intercepted_contents, + std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> + bubble_handle, + bool is_new_profile); + + // Returns the outcome of the interception heuristic. + // If the outcome is kInterceptProfileSwitch, the target profile is returned + // in |entry|. + // In some cases the outcome cannot be fully computed synchronously, when this + // happens, the signin interception is highly likely (but not guaranteed). + absl::optional<SigninInterceptionHeuristicOutcome> GetHeuristicOutcome( + bool is_new_account, + bool is_sync_signin, + const std::string& email, + const ProfileAttributesEntry** entry = nullptr) const; + + // Returns true if the interception is in progress (running the heuristic or + // showing on screen). + bool is_interception_in_progress() const { + return is_interception_in_progress_; + } + + void SetAccountLevelSigninRestrictionFetchResultForTesting( + absl::optional<std::string> value) { + intercepted_account_level_policy_value_fetch_result_for_testing_ = + std::move(value); + } + + // KeyedService: + void Shutdown() override; + + private: + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + ShouldShowProfileSwitchBubble); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + NoBubbleWithSingleAccount); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + ShouldShowEnterpriseBubble); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + ShouldShowEnterpriseBubbleWithoutUPA); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, + ShouldShowMultiUserBubble); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorTest, PersistentHash); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparation); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationWithoutUPA); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationReauth); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + EnforceManagedAccountAsPrimary); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationReauth); + FRIEND_TEST_ALL_PREFIXES(DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTestAccountLevelPolicy); + FRIEND_TEST_ALL_PREFIXES( + DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTestNoForcedInterception); + + // Cancels any current signin interception and resets the interceptor to its + // initial state. + void Reset(); + + // Helper functions to determine which interception UI should be shown. + const ProfileAttributesEntry* ShouldShowProfileSwitchBubble( + const std::string& intercepted_email, + ProfileAttributesStorage* profile_attribute_storage) const; + bool ShouldEnforceEnterpriseProfileSeparation( + const AccountInfo& intercepted_account_info) const; + bool ShouldShowEnterpriseBubble( + const AccountInfo& intercepted_account_info) const; + bool ShouldShowMultiUserBubble( + const AccountInfo& intercepted_account_info) const; + + void OnInterceptionReadyToBeProcessed(const AccountInfo& info); + + // signin::IdentityManager::Observer: + void OnExtendedAccountInfoUpdated(const AccountInfo& info) override; + + // Called when the extended account info was not updated after a timeout. + void OnExtendedAccountInfoFetchTimeout(); + + // Called after the user chose whether a new profile would be created. + void OnProfileCreationChoice(const AccountInfo& account_info, + SkColor profile_color, + SigninInterceptionResult create); + // Called after the user chose whether the session should continue in a new + // profile. + void OnProfileSwitchChoice(const std::string& email, + const base::FilePath& profile_path, + SigninInterceptionResult switch_profile); + + // Called when the new profile is created or loaded from disk. + // `profile_color` is set as theme color for the profile ; it should be + // nullopt if the profile is not new (loaded from disk). + void OnNewSignedInProfileCreated(absl::optional<SkColor> profile_color, + Profile* new_profile); + + // Called after the user choses whether the session should continue in a new + // work profile or not. If the user choses not to continue in a work profile, + // the account is signed out. + void OnEnterpriseProfileCreationResult(const AccountInfo& account_info, + SkColor profile_color, + SigninInterceptionResult create); + + // Called when the new browser is created after interception. Passed as + // callback to `session_startup_helper_`. + void OnNewBrowserCreated(bool is_new_profile); + + // Returns a 8-bit hash of the email that can be persisted. + static std::string GetPersistentEmailHash(const std::string& email); + + // Should be called when the user declines profile creation, in order to + // remember their decision. This information is stored in prefs. Only a hash + // of the email is saved, as Chrome does not need to store the actual email, + // but only need to compare emails. The hash has low entropy to ensure it + // cannot be reversed. + void RecordProfileCreationDeclined(const std::string& email); + + // Checks if the user previously declined 2 times creating a new profile for + // this account. + bool HasUserDeclinedProfileCreation(const std::string& email) const; + + // Fetches the value of the cloud user level value of the + // ManagedAccountsSigninRestriction policy for 'account_info' and runs + // `callback` with the result. This is a network call that has a 5 seconds + // timeout. + void FetchAccountLevelSigninRestrictionForInterceptedAccount( + const AccountInfo& account_info, + base::OnceCallback<void(const std::string&)> callback); + + // Called when the the value of the cloud user level value of the + // ManagedAccountsSigninRestriction is received. + void OnAccountLevelManagedAccountsSigninRestrictionReceived( + bool timed_out, + const AccountInfo& account_info, + const std::string& signin_restriction); + + const raw_ptr<Profile> profile_; + const raw_ptr<signin::IdentityManager> identity_manager_; + std::unique_ptr<Delegate> delegate_; + + // Used in the profile that was created after the interception succeeded. + std::unique_ptr<DiceInterceptedSessionStartupHelper> session_startup_helper_; + + // Members below are related to the interception in progress. + base::WeakPtr<content::WebContents> web_contents_; + bool is_interception_in_progress_ = false; + CoreAccountId account_id_; + bool new_account_interception_ = false; + bool intercepted_account_management_accepted_ = false; + base::ScopedObservation<signin::IdentityManager, + signin::IdentityManager::Observer> + account_info_update_observation_{this}; + // Timeout for the fetch of the extended account info. The signin interception + // is cancelled if the account info cannot be fetched quickly. + base::CancelableOnceCallback<void()> on_account_info_update_timeout_; + std::unique_ptr<DiceSignedInProfileCreator> dice_signed_in_profile_creator_; + // Used to retain the interception UI bubble until profile creation completes. + std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> + interception_bubble_handle_; + // Used for metrics: + bool was_interception_ui_displayed_ = false; + base::TimeTicks account_info_fetch_start_time_; + base::TimeTicks profile_creation_start_time_; + + // Timeout for the fetch of cloud user level policy value of + // ManagedAccountsSigninRestriction. The signin interception continue with an + // empty value for the policy if we cannot get the value. + base::CancelableOnceCallback<void()> + on_intercepted_account_level_policy_value_timeout_; + + // Used to fetch the cloud user level policy value of + // ManagedAccountsSigninRestriction. This can only fetch one policy value for + // one account at the time. + std::unique_ptr<policy::UserCloudSigninRestrictionPolicyFetcher> + account_level_signin_restriction_policy_fetcher_; + // Value of the ManagedAccountsSigninRestriction for the intercepted account. + // If no value is set, then we have not yet received the policy value. + absl::optional<std::string> intercepted_account_level_policy_value_; + absl::optional<std::string> + intercepted_account_level_policy_value_fetch_result_for_testing_; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_H_ diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc new file mode 100644 index 00000000000..5a53ea95baa --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_browsertest.cc @@ -0,0 +1,1200 @@ +// 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. + +#include "chrome/browser/signin/dice_web_signin_interceptor.h" + +#include <map> +#include <string> + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/bind.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/threading/thread_task_runner_handle.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/enterprise/util/managed_browser_utils.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_init_params.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_manager_observer.h" +#include "chrome/browser/profiles/profile_window.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/chrome_signin_client_test_util.h" +#include "chrome/browser/signin/dice_intercepted_session_startup_helper.h" +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/themes/theme_service.h" +#include "chrome/browser/themes/theme_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/profile_waiter.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/account_id/account_id.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/policy/core/common/features.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/gaia_urls.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { + +// Fake response for OAuth multilogin. +const char kMultiloginSuccessResponse[] = + R"()]}' + { + "status": "OK", + "cookies":[ + { + "name":"SID", + "value":"SID_value", + "domain":".google.fr", + "path":"/", + "isSecure":true, + "isHttpOnly":false, + "priority":"HIGH", + "maxAge":63070000 + } + ] + } + )"; + +class FakeDiceWebSigninInterceptorDelegate; + +class FakeBubbleHandle : public ScopedDiceWebSigninInterceptionBubbleHandle, + public base::SupportsWeakPtr<FakeBubbleHandle> { + public: + ~FakeBubbleHandle() override = default; +}; + +// Dummy interception delegate that automatically accepts multi user +// interception. +class FakeDiceWebSigninInterceptorDelegate + : public DiceWebSigninInterceptor::Delegate { + public: + std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> + ShowSigninInterceptionBubble( + content::WebContents* web_contents, + const BubbleParameters& bubble_parameters, + base::OnceCallback<void(SigninInterceptionResult)> callback) override { + EXPECT_EQ(bubble_parameters.interception_type, expected_interception_type_); + auto bubble_handle = std::make_unique<FakeBubbleHandle>(); + weak_bubble_handle_ = bubble_handle->AsWeakPtr(); + // The callback must not be called synchronously (see the documentation for + // ShowSigninInterceptionBubble). + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(std::move(callback), expected_interception_result_)); + return bubble_handle; + } + + void ShowProfileCustomizationBubble(Browser* browser) override { + EXPECT_FALSE(customized_browser_) + << "Customization must be shown only once."; + customized_browser_ = browser; + } + + Browser* customized_browser() { return customized_browser_; } + + void set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType type) { + expected_interception_type_ = type; + } + + void set_expected_interception_result(SigninInterceptionResult result) { + expected_interception_result_ = result; + } + + bool intercept_bubble_shown() const { return weak_bubble_handle_.get(); } + + bool intercept_bubble_destroyed() const { + return weak_bubble_handle_.WasInvalidated(); + } + + private: + raw_ptr<Browser> customized_browser_ = nullptr; + DiceWebSigninInterceptor::SigninInterceptionType expected_interception_type_ = + DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser; + SigninInterceptionResult expected_interception_result_ = + SigninInterceptionResult::kAccepted; + base::WeakPtr<FakeBubbleHandle> weak_bubble_handle_; +}; + +class BrowserCloseObserver : public BrowserListObserver { + public: + explicit BrowserCloseObserver(Browser* browser) : browser_(browser) { + BrowserList::AddObserver(this); + } + + BrowserCloseObserver(const BrowserCloseObserver&) = delete; + BrowserCloseObserver& operator=(const BrowserCloseObserver&) = delete; + + ~BrowserCloseObserver() override { BrowserList::RemoveObserver(this); } + + void Wait() { run_loop_.Run(); } + + // BrowserListObserver implementation. + void OnBrowserRemoved(Browser* browser) override { + if (browser == browser_) + run_loop_.Quit(); + } + + private: + raw_ptr<Browser> browser_; + base::RunLoop run_loop_; +}; + +// Runs the interception and returns the new profile that was created. +Profile* InterceptAndWaitProfileCreation(content::WebContents* contents, + const CoreAccountId& account_id) { + ProfileWaiter profile_waiter; + // Start the interception. + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile( + Profile::FromBrowserContext(contents->GetBrowserContext())); + interceptor->MaybeInterceptWebSignin(contents, account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + // Wait for the interception to be complete. + return profile_waiter.WaitForProfileAdded(); +} + +// Checks that the interception histograms were correctly recorded. +void CheckHistograms(const base::HistogramTester& histogram_tester, + SigninInterceptionHeuristicOutcome outcome, + bool reauth = false) { + int profile_switch_count = + outcome == SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch || + outcome == SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch + ? 1 + : 0; + int profile_creation_count = reauth ? 0 : 1 - profile_switch_count; + int fetched_account_count = + reauth || outcome == SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch + ? 1 + : profile_creation_count; + + histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome", + outcome, 1); + histogram_tester.ExpectTotalCount( + "Signin.Intercept.AccountInfoFetchDuration", + base::FeatureList::IsEnabled( + policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher) + ? 1 + : fetched_account_count); + histogram_tester.ExpectTotalCount("Signin.Intercept.ProfileCreationDuration", + profile_creation_count); + histogram_tester.ExpectTotalCount("Signin.Intercept.ProfileSwitchDuration", + profile_switch_count); +} + +} // namespace + +class DiceWebSigninInterceptorBrowserTest : public InProcessBrowserTest { + public: + DiceWebSigninInterceptorBrowserTest() = default; + + Profile* profile() { return browser()->profile(); } + + signin::IdentityTestEnvironment* identity_test_env() { + return identity_test_env_profile_adaptor_->identity_test_env(); + } + + network::TestURLLoaderFactory* test_url_loader_factory() { + return &test_url_loader_factory_; + } + + content::WebContents* AddTab(const GURL& url) { + ui_test_utils::NavigateToURLWithDisposition( + browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); + return browser()->tab_strip_model()->GetActiveWebContents(); + } + + FakeDiceWebSigninInterceptorDelegate* GetInterceptorDelegate( + Profile* profile) { + // Make sure the interceptor has been created. + DiceWebSigninInterceptorFactory::GetForProfile(profile); + FakeDiceWebSigninInterceptorDelegate* interceptor_delegate = + interceptor_delegates_[profile]; + return interceptor_delegate; + } + + void SetupGaiaResponses() { + // Instantly return from Gaia calls, to avoid timing out when injecting the + // account in the new profile. + network::TestURLLoaderFactory* loader_factory = test_url_loader_factory(); + loader_factory->SetInterceptor(base::BindLambdaForTesting( + [loader_factory](const network::ResourceRequest& request) { + std::string path = request.url.path(); + if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") { + loader_factory->AddResponse(request.url.spec(), std::string()); + return; + } + if (path == "/oauth/multilogin") { + loader_factory->AddResponse(request.url.spec(), + kMultiloginSuccessResponse); + return; + } + })); + } + + private: + // InProcessBrowserTest: + void SetUpOnMainThread() override { + ASSERT_TRUE(embedded_test_server()->Start()); + identity_test_env_profile_adaptor_ = + std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile()); + DiceWebSigninInterceptorFactory::GetForProfile(profile()) + ->SetAccountLevelSigninRestrictionFetchResultForTesting(""); + } + + void TearDownOnMainThread() override { + // Must be destroyed before the Profile. + identity_test_env_profile_adaptor_.reset(); + } + + void SetUpInProcessBrowserTestFixture() override { + InProcessBrowserTest::SetUpInProcessBrowserTestFixture(); + create_services_subscription_ = + BrowserContextDependencyManager::GetInstance() + ->RegisterCreateServicesCallbackForTesting( + base::BindRepeating(&DiceWebSigninInterceptorBrowserTest:: + OnWillCreateBrowserContextServices, + base::Unretained(this))); + } + + void OnWillCreateBrowserContextServices(content::BrowserContext* context) { + IdentityTestEnvironmentProfileAdaptor:: + SetIdentityTestEnvironmentFactoriesOnBrowserContext(context); + ChromeSigninClientFactory::GetInstance()->SetTestingFactory( + context, base::BindRepeating(&BuildChromeSigninClientWithURLLoader, + &test_url_loader_factory_)); + DiceWebSigninInterceptorFactory::GetInstance()->SetTestingFactory( + context, + base::BindRepeating(&DiceWebSigninInterceptorBrowserTest:: + BuildDiceWebSigninInterceptorWithFakeDelegate, + base::Unretained(this))); + } + + // Builds a DiceWebSigninInterceptor with a fake delegate. To be used as a + // testing factory. + std::unique_ptr<KeyedService> BuildDiceWebSigninInterceptorWithFakeDelegate( + content::BrowserContext* context) { + std::unique_ptr<FakeDiceWebSigninInterceptorDelegate> fake_delegate = + std::make_unique<FakeDiceWebSigninInterceptorDelegate>(); + interceptor_delegates_[context] = fake_delegate.get(); + return std::make_unique<DiceWebSigninInterceptor>( + Profile::FromBrowserContext(context), std::move(fake_delegate)); + } + + network::TestURLLoaderFactory test_url_loader_factory_; + std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> + identity_test_env_profile_adaptor_; + base::CallbackListSubscription create_services_subscription_; + std::map<content::BrowserContext*, FakeDiceWebSigninInterceptorDelegate*> + interceptor_delegates_; +}; + +// Tests the complete interception flow including profile and browser creation. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, InterceptionTest) { + base::HistogramTester histogram_tester; + // Setup profile for interception. + identity_test_env()->MakeAccountAvailable("alice@example.com"); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = kNoHostedDomainFound; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + SetupGaiaResponses(); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + ASSERT_TRUE(new_profile); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + IdentityTestEnvironmentProfileAdaptor adaptor(new_profile); + adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true); + + // Check the profile name. + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(new_profile->GetPath()); + ASSERT_TRUE(entry); + EXPECT_EQ("givenname", base::UTF16ToUTF8(entry->GetLocalProfileName())); + // Check the profile color. + EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile) + ->UsingAutogeneratedTheme()); + + // A browser has been created for the new profile and the tab was moved there. + Browser* added_browser = ui_test_utils::WaitForBrowserToOpen(); + ASSERT_TRUE(added_browser); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptMultiUser); + // Interception bubble is destroyed in the source profile, and was not shown + // in the new profile. + FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate = + GetInterceptorDelegate(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed()); + EXPECT_FALSE(new_interceptor_delegate->intercept_bubble_shown()); + EXPECT_FALSE(new_interceptor_delegate->intercept_bubble_destroyed()); + // Profile customization UI was shown exactly once in the new profile. + EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete profile switch flow when the profile is not loaded. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, SwitchAndLoad) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + // Add a profile in the cache (simulate the profile on disk). + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ProfileAttributesStorage* profile_storage = + &profile_manager->GetProfileAttributesStorage(); + const base::FilePath profile_path = + profile_manager->GenerateNextProfileDirectoryPath(); + ProfileAttributesInitParams params; + params.profile_path = profile_path; + params.profile_name = u"TestProfileName"; + params.gaia_id = account_info.gaia; + params.user_name = base::UTF8ToUTF16(account_info.email); + profile_storage->AddProfile(std::move(params)); + ProfileAttributesEntry* entry = + profile_storage->GetProfileAttributesWithPath(profile_path); + ASSERT_TRUE(entry); + ASSERT_EQ(entry->GetGAIAId(), account_info.gaia); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + // Check that the right profile was opened. + EXPECT_EQ(new_profile->GetPath(), profile_path); + + // Add the account to the cookies (simulates the account reconcilor). + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // A browser has been created for the new profile and the tab was moved there. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* added_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(added_browser); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); + // Interception bubble was closed. + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed()); + // Profile customization was not shown. + EXPECT_EQ(GetInterceptorDelegate(new_profile)->customized_browser(), nullptr); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete profile switch flow when the profile is already loaded. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, SwitchAlreadyOpen) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + // Create another profile with a browser window. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + const base::FilePath profile_path = + profile_manager->GenerateNextProfileDirectoryPath(); + base::RunLoop loop; + Profile* other_profile = nullptr; + ProfileManager::CreateCallback callback = base::BindLambdaForTesting( + [&other_profile, &loop](Profile* profile, Profile::CreateStatus status) { + DCHECK_EQ(status, Profile::CREATE_STATUS_INITIALIZED); + other_profile = profile; + loop.Quit(); + }); + profiles::SwitchToProfile(profile_path, /*always_create=*/true, + std::move(callback)); + loop.Run(); + ASSERT_TRUE(other_profile); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* other_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(other_browser); + ASSERT_EQ(other_browser->profile(), other_profile); + // Add the account to the other profile. + signin::IdentityManager* other_identity_manager = + IdentityManagerFactory::GetForProfile(other_profile); + other_identity_manager->GetAccountsMutator()->AddOrUpdateAccount( + account_info.gaia, account_info.email, "dummy_refresh_token", + /*is_under_advanced_protection=*/false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + other_identity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount( + account_info.account_id, signin::ConsentLevel::kSync); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + int other_original_tab_count = other_browser->tab_strip_model()->count(); + + // Start the interception. + GetInterceptorDelegate(profile())->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch); + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile(profile()); + interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + + // Add the account to the cookies (simulates the account reconcilor). + signin::SetCookieAccounts(other_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // The tab was moved to the new browser. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(other_browser->tab_strip_model()->count(), + other_original_tab_count + 1); + EXPECT_EQ(other_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); + // Profile customization was not shown. + EXPECT_EQ(GetInterceptorDelegate(other_profile)->customized_browser(), + nullptr); + EXPECT_EQ(GetInterceptorDelegate(profile())->customized_browser(), nullptr); +} + +// Close the source tab during the interception and check that the NTP is opened +// in the new profile (regression test for https://crbug.com/1153321). +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorBrowserTest, CloseSourceTab) { + // Setup profile for interception. + identity_test_env()->MakeAccountAvailable("alice@example.com"); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = kNoHostedDomainFound; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + ProfileWaiter profile_waiter; + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile( + Profile::FromBrowserContext(contents->GetBrowserContext())); + interceptor->MaybeInterceptWebSignin(contents, account_info.account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + // Close the source tab during the profile creation. + contents->Close(); + // Wait for the interception to be complete. + Profile* new_profile = profile_waiter.WaitForProfileAdded(); + ASSERT_TRUE(new_profile); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + // Add the account to the cookies (simulates the account reconcilor). + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // A browser has been created for the new profile on the new tab page. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* added_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(added_browser); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + GURL("chrome://newtab/")); +} + +class DiceWebSigninInterceptorEnterpriseBrowserTest + : public DiceWebSigninInterceptorBrowserTest { + public: + DiceWebSigninInterceptorEnterpriseBrowserTest() { + enterprise_feature_list_.InitWithFeatures( + {kAccountPoliciesLoadedWithoutSync, + policy::features::kEnableUserCloudSigninRestrictionPolicyFetcher}, + {}); + } + + private: + base::test::ScopedFeatureList enterprise_feature_list_; +}; + +// Tests the complete interception flow including profile and browser creation. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTestNoForcedInterception) { + base::HistogramTester histogram_tester; + + AccountInfo primary_account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + // Fill the account info, in particular for the hosted_domain field. + primary_account_info.full_name = "fullname"; + primary_account_info.given_name = "givenname"; + primary_account_info.hosted_domain = "example.com"; + primary_account_info.locale = "en"; + primary_account_info.picture_url = "https://example.com"; + DCHECK(primary_account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); + IdentityManagerFactory::GetForProfile(profile()) + ->GetPrimaryAccountMutator() + ->SetPrimaryAccount(primary_account_info.account_id, + signin::ConsentLevel::kSync); + + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Enforce enterprise profile sepatation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "none"); + DiceWebSigninInterceptorFactory::GetForProfile(profile()) + ->SetAccountLevelSigninRestrictionFetchResultForTesting(""); + + // Instantly return from Gaia calls, to avoid timing out when injecting the + // account in the new profile. + network::TestURLLoaderFactory* loader_factory = test_url_loader_factory(); + loader_factory->SetInterceptor(base::BindLambdaForTesting( + [loader_factory](const network::ResourceRequest& request) { + std::string path = request.url.path(); + if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") { + loader_factory->AddResponse(request.url.spec(), std::string()); + return; + } + if (path == "/oauth/multilogin") { + loader_factory->AddResponse(request.url.spec(), + kMultiloginSuccessResponse); + return; + } + })); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + EXPECT_FALSE( + chrome::enterprise_util::UserAcceptedAccountManagement(new_profile)); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + IdentityTestEnvironmentProfileAdaptor adaptor(new_profile); + adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true); + + // Check the profile name. + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(new_profile->GetPath()); + ASSERT_TRUE(entry); + EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName())); + // Check the profile color. + EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile) + ->UsingAutogeneratedTheme()); + + // A browser has been created for the new profile and the tab was moved there. + Browser* added_browser = ui_test_utils::WaitForBrowserToOpen(); + ASSERT_TRUE(added_browser); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptEnterprise); + // Interception bubble is destroyed in the source profile, and was not shown + // in the new profile. + FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate = + GetInterceptorDelegate(new_profile); + + // Profile customization UI was shown exactly once in the new profile. + EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete interception flow including profile and browser creation. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTestAccountLevelPolicy) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Enforce enterprise profile sepatation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "none"); + DiceWebSigninInterceptorFactory::GetForProfile(profile()) + ->SetAccountLevelSigninRestrictionFetchResultForTesting( + "primary_account"); + + // Instantly return from Gaia calls, to avoid timing out when injecting the + // account in the new profile. + network::TestURLLoaderFactory* loader_factory = test_url_loader_factory(); + loader_factory->SetInterceptor(base::BindLambdaForTesting( + [loader_factory](const network::ResourceRequest& request) { + std::string path = request.url.path(); + if (path == "/ListAccounts" || path == "/GetCheckConnectionInfo") { + loader_factory->AddResponse(request.url.spec(), std::string()); + return; + } + if (path == "/oauth/multilogin") { + loader_factory->AddResponse(request.url.spec(), + kMultiloginSuccessResponse); + return; + } + })); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + EXPECT_TRUE( + chrome::enterprise_util::UserAcceptedAccountManagement(new_profile)); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + IdentityTestEnvironmentProfileAdaptor adaptor(new_profile); + adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true); + + // Check the profile name. + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(new_profile->GetPath()); + ASSERT_TRUE(entry); + EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName())); + // Check the profile color. + EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile) + ->UsingAutogeneratedTheme()); + + // A browser has been created for the new profile and the tab was moved there. + Browser* added_browser = ui_test_utils::WaitForBrowserToOpen(); + ASSERT_TRUE(added_browser); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms( + histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); + // Interception bubble is destroyed in the source profile, and was not shown + // in the new profile. + FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate = + GetInterceptorDelegate(new_profile); + + // Profile customization UI was shown exactly once in the new profile. + EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete interception flow including profile and browser creation. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionTest) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + SetupGaiaResponses(); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + EXPECT_TRUE( + chrome::enterprise_util::UserAcceptedAccountManagement(new_profile)); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + IdentityTestEnvironmentProfileAdaptor adaptor(new_profile); + adaptor.identity_test_env()->SetAutomaticIssueOfAccessTokens(true); + + // Check the profile name. + ProfileAttributesStorage& storage = + g_browser_process->profile_manager()->GetProfileAttributesStorage(); + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(new_profile->GetPath()); + ASSERT_TRUE(entry); + EXPECT_EQ("example.com", base::UTF16ToUTF8(entry->GetLocalProfileName())); + // Check the profile color. + EXPECT_TRUE(ThemeServiceFactory::GetForProfile(new_profile) + ->UsingAutogeneratedTheme()); + + // A browser has been created for the new profile and the tab was moved there. + Browser* added_browser = ui_test_utils::WaitForBrowserToOpen(); + ASSERT_TRUE(added_browser); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms( + histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); + // Interception bubble is destroyed in the source profile, and was not shown + // in the new profile. + FakeDiceWebSigninInterceptorDelegate* new_interceptor_delegate = + GetInterceptorDelegate(new_profile); + + // Profile customization UI was shown exactly once in the new profile. + EXPECT_EQ(new_interceptor_delegate->customized_browser(), added_browser); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Tests the complete interception flow for a reauth of the primary account of a +// non-syncing profile. +IN_PROC_BROWSER_TEST_F( + DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionPrimaryACcountReauthSyncDisabledTest) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + IdentityManagerFactory::GetForProfile(profile()) + ->GetPrimaryAccountMutator() + ->SetPrimaryAccount(account_info.account_id, + signin::ConsentLevel::kSignin); + + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + SetupGaiaResponses(); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced); + + EXPECT_FALSE( + chrome::enterprise_util::UserAcceptedAccountManagement(profile())); + // Start the interception. + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile(profile()); + interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id, + /*is_new_account=*/false, + /*is_sync_signin=*/false); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE( + chrome::enterprise_util::UserAcceptedAccountManagement(profile())); + // Interception bubble was closed. + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed()); + EXPECT_TRUE(IdentityManagerFactory::GetForProfile(profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + + ASSERT_EQ(BrowserList::GetInstance()->size(), 1u); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count); + EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms( + histogram_tester, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced, + /*reauth=*/true); +} + +// Tests the complete interception flow for a reauth of the primary account of a +// syncing profile. +IN_PROC_BROWSER_TEST_F( + DiceWebSigninInterceptorEnterpriseBrowserTest, + ForcedEnterpriseInterceptionPrimaryACcountReauthSyncEnabledTest) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + IdentityManagerFactory::GetForProfile(profile()) + ->GetPrimaryAccountMutator() + ->SetPrimaryAccount(account_info.account_id, signin::ConsentLevel::kSync); + + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + SetupGaiaResponses(); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced); + + EXPECT_FALSE( + chrome::enterprise_util::UserAcceptedAccountManagement(profile())); + // Start the interception. + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile(profile()); + interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id, + /*is_new_account=*/false, + /*is_sync_signin=*/false); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE( + chrome::enterprise_util::UserAcceptedAccountManagement(profile())); + // Interception bubble was closed. + EXPECT_FALSE(source_interceptor_delegate->intercept_bubble_shown()); + EXPECT_FALSE(source_interceptor_delegate->intercept_bubble_destroyed()); + EXPECT_TRUE(IdentityManagerFactory::GetForProfile(profile()) + ->HasAccountWithRefreshToken(account_info.account_id)); + + ASSERT_EQ(BrowserList::GetInstance()->size(), 1u); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count); + EXPECT_EQ(browser()->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome::kAbortAccountNotNew, + /*reauth=*/true); +} + +// Tests the complete profile switch flow when the profile is not loaded. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + EnterpriseSwitchAndLoad) { + base::HistogramTester histogram_tester; + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Add a profile in the cache (simulate the profile on disk). + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ProfileAttributesStorage* profile_storage = + &profile_manager->GetProfileAttributesStorage(); + const base::FilePath profile_path = + profile_manager->GenerateNextProfileDirectoryPath(); + ProfileAttributesInitParams params; + params.profile_path = profile_path; + params.profile_name = u"TestProfileName"; + params.gaia_id = account_info.gaia; + params.user_name = base::UTF8ToUTF16(account_info.email); + profile_storage->AddProfile(std::move(params)); + ProfileAttributesEntry* entry = + profile_storage->GetProfileAttributesWithPath(profile_path); + ASSERT_TRUE(entry); + ASSERT_EQ(entry->GetGAIAId(), account_info.gaia); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + + // Do the signin interception. + FakeDiceWebSigninInterceptorDelegate* source_interceptor_delegate = + GetInterceptorDelegate(profile()); + source_interceptor_delegate->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced); + Profile* new_profile = + InterceptAndWaitProfileCreation(web_contents, account_info.account_id); + ASSERT_TRUE(new_profile); + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_shown()); + signin::IdentityManager* new_identity_manager = + IdentityManagerFactory::GetForProfile(new_profile); + EXPECT_TRUE(new_identity_manager->HasAccountWithRefreshToken( + account_info.account_id)); + + // Check that the right profile was opened. + EXPECT_EQ(new_profile->GetPath(), profile_path); + + // Add the account to the cookies (simulates the account reconcilor). + EXPECT_EQ(BrowserList::GetInstance()->size(), 1u); + signin::SetCookieAccounts(new_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // A browser has been created for the new profile and the tab was moved there. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* added_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(added_browser); + EXPECT_EQ(added_browser->profile(), new_profile); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(added_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch); + + // Interception bubble was closed. + EXPECT_TRUE(source_interceptor_delegate->intercept_bubble_destroyed()); + + // Profile customization was not shown. + EXPECT_EQ(GetInterceptorDelegate(new_profile)->customized_browser(), nullptr); + EXPECT_EQ(source_interceptor_delegate->customized_browser(), nullptr); +} + +// Failed run on MAC CI builder. https://crbug.com/1245200 +#if defined(OS_MAC) +#define MAYBE_EnterpriseSwitchAlreadyOpen DISABLED_EnterpriseSwitchAlreadyOpen +#else +#define MAYBE_EnterpriseSwitchAlreadyOpen EnterpriseSwitchAlreadyOpen +#endif +// Tests the complete profile switch flow when the profile is already loaded. +IN_PROC_BROWSER_TEST_F(DiceWebSigninInterceptorEnterpriseBrowserTest, + MAYBE_EnterpriseSwitchAlreadyOpen) { + base::HistogramTester histogram_tester; + // Enforce enterprise profile separation. + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + // Fill the account info, in particular for the hosted_domain field. + account_info.full_name = "fullname"; + account_info.given_name = "givenname"; + account_info.hosted_domain = "example.com"; + account_info.locale = "en"; + account_info.picture_url = "https://example.com"; + DCHECK(account_info.IsValid()); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + // Create another profile with a browser window. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + const base::FilePath profile_path = + profile_manager->GenerateNextProfileDirectoryPath(); + base::RunLoop loop; + Profile* other_profile = nullptr; + ProfileManager::CreateCallback callback = base::BindLambdaForTesting( + [&other_profile, &loop](Profile* profile, Profile::CreateStatus status) { + DCHECK_EQ(status, Profile::CREATE_STATUS_INITIALIZED); + other_profile = profile; + loop.Quit(); + }); + profiles::SwitchToProfile(profile_path, /*always_create=*/true, + std::move(callback)); + loop.Run(); + ASSERT_TRUE(other_profile); + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + Browser* other_browser = BrowserList::GetInstance()->get(1); + ASSERT_TRUE(other_browser); + ASSERT_EQ(other_browser->profile(), other_profile); + // Add the account to the other profile. + signin::IdentityManager* other_identity_manager = + IdentityManagerFactory::GetForProfile(other_profile); + other_identity_manager->GetAccountsMutator()->AddOrUpdateAccount( + account_info.gaia, account_info.email, "dummy_refresh_token", + /*is_under_advanced_protection=*/false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + other_identity_manager->GetPrimaryAccountMutator()->SetPrimaryAccount( + account_info.account_id, signin::ConsentLevel::kSync); + + // Add a tab. + GURL intercepted_url = embedded_test_server()->GetURL("/defaultresponse"); + content::WebContents* web_contents = AddTab(intercepted_url); + int original_tab_count = browser()->tab_strip_model()->count(); + int other_original_tab_count = other_browser->tab_strip_model()->count(); + + // Start the interception. + GetInterceptorDelegate(profile())->set_expected_interception_type( + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced); + DiceWebSigninInterceptor* interceptor = + DiceWebSigninInterceptorFactory::GetForProfile(profile()); + interceptor->MaybeInterceptWebSignin(web_contents, account_info.account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + + // Add the account to the cookies (simulates the account reconcilor). + signin::SetCookieAccounts(other_identity_manager, test_url_loader_factory(), + {{account_info.email, account_info.gaia}}); + + // The tab was moved to the new browser. + ASSERT_EQ(BrowserList::GetInstance()->size(), 2u); + EXPECT_EQ(browser()->tab_strip_model()->count(), original_tab_count - 1); + EXPECT_EQ(other_browser->tab_strip_model()->count(), + other_original_tab_count + 1); + EXPECT_EQ(other_browser->tab_strip_model()->GetActiveWebContents()->GetURL(), + intercepted_url); + + CheckHistograms(histogram_tester, + SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch); + // Profile customization was not shown. + EXPECT_EQ(GetInterceptorDelegate(other_profile)->customized_browser(), + nullptr); + EXPECT_EQ(GetInterceptorDelegate(profile())->customized_browser(), nullptr); +} diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc new file mode 100644 index 00000000000..9377d94d2a8 --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.cc @@ -0,0 +1,45 @@ +// 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. + +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/dice_web_signin_interceptor.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/signin/dice_web_signin_interceptor_delegate.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +// static +DiceWebSigninInterceptor* DiceWebSigninInterceptorFactory::GetForProfile( + Profile* profile) { + return static_cast<DiceWebSigninInterceptor*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +DiceWebSigninInterceptorFactory* +DiceWebSigninInterceptorFactory::GetInstance() { + return base::Singleton<DiceWebSigninInterceptorFactory>::get(); +} + +DiceWebSigninInterceptorFactory::DiceWebSigninInterceptorFactory() + : BrowserContextKeyedServiceFactory( + "DiceWebSigninInterceptor", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +DiceWebSigninInterceptorFactory::~DiceWebSigninInterceptorFactory() = default; + +void DiceWebSigninInterceptorFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + DiceWebSigninInterceptor::RegisterProfilePrefs(registry); +} + +KeyedService* DiceWebSigninInterceptorFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new DiceWebSigninInterceptor( + Profile::FromBrowserContext(context), + std::make_unique<DiceWebSigninInterceptorDelegate>()); +} diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h new file mode 100644 index 00000000000..ad6aac1ca3e --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_factory.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class DiceWebSigninInterceptor; +class Profile; + +class DiceWebSigninInterceptorFactory + : public BrowserContextKeyedServiceFactory { + public: + static DiceWebSigninInterceptor* GetForProfile(Profile* profile); + static DiceWebSigninInterceptorFactory* GetInstance(); + + DiceWebSigninInterceptorFactory(const DiceWebSigninInterceptorFactory&) = + delete; + DiceWebSigninInterceptorFactory& operator=( + const DiceWebSigninInterceptorFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits<DiceWebSigninInterceptorFactory>; + DiceWebSigninInterceptorFactory(); + ~DiceWebSigninInterceptorFactory() override; + + // BrowserContextKeyedServiceFactory: + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_DICE_WEB_SIGNIN_INTERCEPTOR_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc b/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc new file mode 100644 index 00000000000..1f66d5d96ed --- /dev/null +++ b/chromium/chrome/browser/signin/dice_web_signin_interceptor_unittest.cc @@ -0,0 +1,953 @@ +// 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. + +#include "chrome/browser/signin/dice_web_signin_interceptor.h" + +#include <memory> + +#include "base/callback.h" +#include "base/memory/raw_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/chrome_signin_client_test_util.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/browser_with_test_window_test.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/gurl.h" + +namespace { + +class MockDiceWebSigninInterceptorDelegate + : public DiceWebSigninInterceptor::Delegate { + public: + MOCK_METHOD(std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle>, + ShowSigninInterceptionBubble, + (content::WebContents * web_contents, + const BubbleParameters& bubble_parameters, + base::OnceCallback<void(SigninInterceptionResult)> callback), + (override)); + void ShowProfileCustomizationBubble(Browser* browser) override {} +}; + +// Matches BubbleParameters fields excepting the color. This is useful in the +// test because the color is randomly generated. +testing::Matcher<const DiceWebSigninInterceptor::Delegate::BubbleParameters&> +MatchBubbleParameters( + const DiceWebSigninInterceptor::Delegate::BubbleParameters& parameters) { + return testing::AllOf( + testing::Field("interception_type", + &DiceWebSigninInterceptor::Delegate::BubbleParameters:: + interception_type, + parameters.interception_type), + testing::Field("intercepted_account", + &DiceWebSigninInterceptor::Delegate::BubbleParameters:: + intercepted_account, + parameters.intercepted_account), + testing::Field("primary_account", + &DiceWebSigninInterceptor::Delegate::BubbleParameters:: + primary_account, + parameters.primary_account)); +} + +// If the account info is valid, does nothing. Otherwise fills the extended +// fields with default values. +void MakeValidAccountInfo(AccountInfo* info) { + if (info->IsValid()) + return; + info->full_name = "fullname"; + info->given_name = "givenname"; + info->hosted_domain = kNoHostedDomainFound; + info->locale = "en"; + info->picture_url = "https://example.com"; + DCHECK(info->IsValid()); +} + +} // namespace + +class DiceWebSigninInterceptorTest : public BrowserWithTestWindowTest { + public: + DiceWebSigninInterceptorTest() = default; + ~DiceWebSigninInterceptorTest() override = default; + + DiceWebSigninInterceptor* interceptor() { + return dice_web_signin_interceptor_.get(); + } + + MockDiceWebSigninInterceptorDelegate* mock_delegate() { + return mock_delegate_; + } + + content::WebContents* web_contents() { + return browser()->tab_strip_model()->GetActiveWebContents(); + } + + ProfileAttributesStorage* profile_attributes_storage() { + return profile_manager()->profile_attributes_storage(); + } + + signin::IdentityTestEnvironment* identity_test_env() { + return identity_test_env_profile_adaptor_->identity_test_env(); + } + + Profile* CreateTestingProfile(const std::string& name) { + return profile_manager()->CreateTestingProfile(name); + } + + // Helper function that calls MaybeInterceptWebSignin with parameters + // compatible with interception. + void MaybeIntercept(CoreAccountId account_id) { + interceptor()->MaybeInterceptWebSignin(web_contents(), account_id, + /*is_new_account=*/true, + /*is_sync_signin=*/false); + } + + // Calls MaybeInterceptWebSignin and verifies the heuristic outcome, the + // histograms and whether the interception is in progress. + // This function only works if the interception decision can be made + // synchronously (GetHeuristicOutcome() returns a value). + void TestSynchronousInterception( + AccountInfo account_info, + bool is_new_account, + bool is_sync_signin, + SigninInterceptionHeuristicOutcome expected_outcome) { + ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin, + account_info.email, + /*entry=*/nullptr), + expected_outcome); + base::HistogramTester histogram_tester; + interceptor()->MaybeInterceptWebSignin(web_contents(), + account_info.account_id, + is_new_account, is_sync_signin); + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome", + expected_outcome, 1); + EXPECT_EQ(interceptor()->is_interception_in_progress(), + SigninInterceptionHeuristicOutcomeIsSuccess(expected_outcome)); + } + + // Calls MaybeInterceptWebSignin and verifies the heuristic outcome and the + // histograms. + // This function only works if the interception decision cannot be made + // synchronously (GetHeuristicOutcome() returns no value). + void TestAsynchronousInterception( + AccountInfo account_info, + bool is_new_account, + bool is_sync_signin, + SigninInterceptionHeuristicOutcome expected_outcome) { + ASSERT_EQ(interceptor()->GetHeuristicOutcome(is_new_account, is_sync_signin, + account_info.email, + /*entry=*/nullptr), + absl::nullopt); + base::HistogramTester histogram_tester; + interceptor()->MaybeInterceptWebSignin(web_contents(), + account_info.account_id, + is_new_account, is_sync_signin); + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + histogram_tester.ExpectUniqueSample("Signin.Intercept.HeuristicOutcome", + expected_outcome, 1); + EXPECT_TRUE(interceptor()->is_interception_in_progress()); + } + + private: + // testing::Test: + void SetUp() override { + BrowserWithTestWindowTest::SetUp(); + + identity_test_env_profile_adaptor_ = + std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile()); + identity_test_env_profile_adaptor_->identity_test_env() + ->SetTestURLLoaderFactory(&test_url_loader_factory_); + + auto delegate = std::make_unique< + testing::StrictMock<MockDiceWebSigninInterceptorDelegate>>(); + mock_delegate_ = delegate.get(); + dice_web_signin_interceptor_ = std::make_unique<DiceWebSigninInterceptor>( + profile(), std::move(delegate)); + + // Create the first tab so that web_contents() exists. + AddTab(browser(), GURL("http://foo/1")); + } + + void TearDown() override { + dice_web_signin_interceptor_->Shutdown(); + identity_test_env_profile_adaptor_.reset(); + BrowserWithTestWindowTest::TearDown(); + } + + TestingProfile::TestingFactories GetTestingFactories() override { + TestingProfile::TestingFactories factories = + IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories(); + factories.push_back( + {ChromeSigninClientFactory::GetInstance(), + base::BindRepeating(&BuildChromeSigninClientWithURLLoader, + &test_url_loader_factory_)}); + return factories; + } + + network::TestURLLoaderFactory test_url_loader_factory_; + std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> + identity_test_env_profile_adaptor_; + std::unique_ptr<DiceWebSigninInterceptor> dice_web_signin_interceptor_; + raw_ptr<MockDiceWebSigninInterceptorDelegate> mock_delegate_ = nullptr; +}; + +TEST_F(DiceWebSigninInterceptorTest, ShouldShowProfileSwitchBubble) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + const std::string& email = account_info.email; + EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( + email, profile_attributes_storage())); + + // Add another profile with no account. + CreateTestingProfile("Profile 1"); + EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( + email, profile_attributes_storage())); + + // Add another profile with a different account. + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + std::string kOtherGaiaID = "SomeOtherGaiaID"; + ASSERT_NE(kOtherGaiaID, account_info.gaia); + entry->SetAuthInfo(kOtherGaiaID, u"alice@gmail.com", + /*is_consented_primary_account=*/true); + EXPECT_FALSE(interceptor()->ShouldShowProfileSwitchBubble( + email, profile_attributes_storage())); + + // Change the account to match. + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + const ProfileAttributesEntry* switch_to_entry = + interceptor()->ShouldShowProfileSwitchBubble( + email, profile_attributes_storage()); + EXPECT_EQ(entry, switch_to_entry); +} + +TEST_F(DiceWebSigninInterceptorTest, NoBubbleWithSingleAccount) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Without UPA. + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info)); + + // With UPA. + identity_test_env()->SetPrimaryAccount("bob@example.com", + signin::ConsentLevel::kSignin); + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); +} + +TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubble) { + // Setup 3 accounts in the profile: + // - primary account + // - other enterprise account that is not primary (should be ignored) + // - intercepted account. + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "alice@example.com", signin::ConsentLevel::kSignin); + AccountInfo other_account_info = + identity_test_env()->MakeAccountAvailable("dummy@example.com"); + MakeValidAccountInfo(&other_account_info); + other_account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(other_account_info); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId( + signin::ConsentLevel::kSignin), + primary_account_info.account_id); + + // The primary account does not have full account info (empty domain). + ASSERT_TRUE(identity_test_env() + ->identity_manager() + ->FindExtendedAccountInfo(primary_account_info) + .hosted_domain.empty()); + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + + // The primary account has full info. + MakeValidAccountInfo(&primary_account_info); + identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); + // The intercepted account is enterprise. + EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + // Two consummer accounts. + account_info.hosted_domain = kNoHostedDomainFound; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info)); + // The primary account is enterprise. + primary_account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); + EXPECT_TRUE(interceptor()->ShouldShowEnterpriseBubble(account_info)); +} + +class DiceWebSigninInterceptorForcedSeparationTest + : public DiceWebSigninInterceptorTest { + public: + DiceWebSigninInterceptorForcedSeparationTest() + : feature_list_(kAccountPoliciesLoadedWithoutSync) {} + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparation) { + profile()->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + // Setup 3 accounts in the profile: + // - primary account + // - other enterprise account that is not primary (should be ignored) + // - intercepted account. + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "alice@gmail.com", signin::ConsentLevel::kSignin); + + AccountInfo other_account_info = + identity_test_env()->MakeAccountAvailable("dummy@example.com"); + MakeValidAccountInfo(&other_account_info); + other_account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(other_account_info); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + ASSERT_EQ(identity_test_env()->identity_manager()->GetPrimaryAccountId( + signin::ConsentLevel::kSignin), + primary_account_info.account_id); + interceptor()->new_account_interception_ = true; + // Consumer account not intercepted. + EXPECT_FALSE( + interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info)); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + // Managed account intercepted. + EXPECT_TRUE( + interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info)); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationWithoutUPA) { + profile()->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + AccountInfo account_info_1 = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info_1); + account_info_1.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + + interceptor()->new_account_interception_ = true; + // Primary account is not set. + ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount( + signin::ConsentLevel::kSignin)); + EXPECT_TRUE( + interceptor()->ShouldEnforceEnterpriseProfileSeparation(account_info_1)); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + ShouldEnforceEnterpriseProfileSeparationReauth) { + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "alice@example.com", signin::ConsentLevel::kSignin); + MakeValidAccountInfo(&primary_account_info); + primary_account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(primary_account_info); + + // Primary account is set. + ASSERT_TRUE(identity_test_env()->identity_manager()->HasPrimaryAccount( + signin::ConsentLevel::kSignin)); + EXPECT_TRUE(interceptor()->ShouldEnforceEnterpriseProfileSeparation( + primary_account_info)); + + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile()->GetPath()); + entry->SetUserAcceptedAccountManagement(true); + + EXPECT_FALSE(interceptor()->ShouldEnforceEnterpriseProfileSeparation( + primary_account_info)); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + EnforceManagedAccountAsPrimaryReauth) { + profile()->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + + // Reauth intercepted if enterprise confirmation not shown yet for forced + // managed separation. + AccountInfo account_info = identity_test_env()->MakePrimaryAccountAvailable( + "alice@example.com", signin::ConsentLevel::kSignin); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + + // Check that interception works otherwise, as a sanity check. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced, + account_info, account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + + TestAsynchronousInterception( + account_info, /*is_new_account=*/false, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + EnforceManagedAccountAsPrimaryManaged) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + // Check that interception works otherwise, as a sanity check. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterpriseForced, + account_info, AccountInfo(), SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + TestAsynchronousInterception( + account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kInterceptEnterpriseForced); +} + +TEST_F(DiceWebSigninInterceptorForcedSeparationTest, + EnforceManagedAccountAsPrimaryProfileSwitch) { + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + profile()->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + profile()->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + + // Setup for profile switch interception. + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(account_info.email), + /*is_consented_primary_account=*/false); + // Check that interception works otherwise, as a sanity check. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitchForced, + account_info, AccountInfo(), SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + TestAsynchronousInterception(account_info, /*is_new_account=*/true, + /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome:: + kInterceptEnterpriseForcedProfileSwitch); +} + +TEST_F(DiceWebSigninInterceptorTest, ShouldShowEnterpriseBubbleWithoutUPA) { + AccountInfo account_info_1 = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info_1); + account_info_1.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + AccountInfo account_info_2 = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info_2); + account_info_2.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_2); + + // Primary account is not set. + ASSERT_FALSE(identity_test_env()->identity_manager()->HasPrimaryAccount( + signin::ConsentLevel::kSignin)); + EXPECT_FALSE(interceptor()->ShouldShowEnterpriseBubble(account_info_1)); +} + +TEST_F(DiceWebSigninInterceptorTest, ShouldShowMultiUserBubble) { + // Setup two accounts in the profile. + AccountInfo account_info_1 = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + MakeValidAccountInfo(&account_info_1); + account_info_1.given_name = "Bob"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + AccountInfo account_info_2 = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + + // The other account does not have full account info (empty name). + ASSERT_TRUE(account_info_2.given_name.empty()); + EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); + + // Accounts with different names. + account_info_1.given_name = "Bob"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + MakeValidAccountInfo(&account_info_2); + account_info_2.given_name = "Alice"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_2); + EXPECT_TRUE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); + + // Accounts with same names. + account_info_1.given_name = "Alice"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); + + // Comparison is case insensitive. + account_info_1.given_name = "alice"; + identity_test_env()->UpdateAccountInfoForAccount(account_info_1); + EXPECT_FALSE(interceptor()->ShouldShowMultiUserBubble(account_info_1)); +} + +TEST_F(DiceWebSigninInterceptorTest, NoInterception) { + // Setup for profile switch interception. + std::string email = "bob@example.com"; + AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + + // Check that Sync signin is not intercepted. + TestSynchronousInterception( + account_info, /*is_new_account=*/true, /*is_sync_signin=*/true, + SigninInterceptionHeuristicOutcome::kAbortSyncSignin); + + // Check that reauth is not intercepted. + TestSynchronousInterception( + account_info, /*is_new_account=*/false, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kAbortAccountNotNew); + + // Check that interception works otherwise, as a sanity check. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, + account_info, AccountInfo(), SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + TestSynchronousInterception( + account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); +} + +// Checks that the heuristic still works if the account was not added to Chrome +// yet. +TEST_F(DiceWebSigninInterceptorTest, HeuristicAccountNotAdded) { + // Setup for profile switch interception. + std::string email = "bob@example.com"; + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + EXPECT_EQ(interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, email, + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); +} + +// Checks that the heuristic defaults to gmail.com when no domain is specified. +TEST_F(DiceWebSigninInterceptorTest, HeuristicDefaultsToGmail) { + // Setup for profile switch interception. + std::string email = "bob@gmail.com"; + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + // No domain defaults to gmail.com + EXPECT_EQ(interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, "bob", + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch); + // Using wrong domain does not trigger the interception. + EXPECT_EQ( + interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com", + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kAbortSingleAccount); +} + +// Checks that no heuristic is returned if signin interception is disabled. +TEST_F(DiceWebSigninInterceptorTest, InterceptionDisabled) { + // Setup for profile switch interception. + std::string email = "bob@gmail.com"; + Profile* profile_2 = CreateTestingProfile("Profile 2"); + profile()->GetPrefs()->SetBoolean(prefs::kSigninInterceptionEnabled, false); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo("dummy_gaia_id", base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + EXPECT_EQ(interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, "bob", + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled); + EXPECT_EQ( + interceptor()->GetHeuristicOutcome( + /*is_new_account=*/true, /*is_sync_signin=*/false, "bob@example.com", + /*entry=*/nullptr), + SigninInterceptionHeuristicOutcome::kAbortInterceptionDisabled); +} + +TEST_F(DiceWebSigninInterceptorTest, TabClosed) { + base::HistogramTester histogram_tester; + interceptor()->MaybeInterceptWebSignin( + /*web_contents=*/nullptr, CoreAccountId(), + /*is_new_account=*/true, /*is_sync_signin=*/false); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kAbortTabClosed, 1); +} + +TEST_F(DiceWebSigninInterceptorTest, InterceptionInProgress) { + // Setup for profile switch interception. + std::string email = "bob@example.com"; + AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + + // Start an interception. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, + account_info, AccountInfo(), SkColor()}; + base::OnceCallback<void(SigninInterceptionResult)> delegate_callback; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)) + .WillOnce(testing::WithArg<2>(testing::Invoke( + [&delegate_callback]( + base::OnceCallback<void(SigninInterceptionResult)> callback) { + delegate_callback = std::move(callback); + return nullptr; + }))); + MaybeIntercept(account_info.account_id); + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + EXPECT_TRUE(interceptor()->is_interception_in_progress()); + + // Check that there is no interception while another one is in progress. + base::HistogramTester histogram_tester; + MaybeIntercept(account_info.account_id); + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kAbortInterceptInProgress, 1); + + // Complete the interception that was in progress. + std::move(delegate_callback).Run(SigninInterceptionResult::kDeclined); + EXPECT_FALSE(interceptor()->is_interception_in_progress()); + + // A new interception can now start. + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); +} + +TEST_F(DiceWebSigninInterceptorTest, DeclineCreationRepeatedly) { + base::HistogramTester histogram_tester; + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "bob@example.com", signin::ConsentLevel::kSignin); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + const int kMaxProfileCreationDeclinedCount = 2; + // Decline the interception kMaxProfileCreationDeclinedCount times. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, + account_info, primary_account_info, SkColor()}; + for (int i = 0; i < kMaxProfileCreationDeclinedCount; ++i) { + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)) + .WillOnce(testing::WithArg<2>(testing::Invoke( + [](base::OnceCallback<void(SigninInterceptionResult)> callback) { + std::move(callback).Run(SigninInterceptionResult::kDeclined); + return nullptr; + }))); + MaybeIntercept(account_info.account_id); + EXPECT_EQ(interceptor()->is_interception_in_progress(), false); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptEnterprise, i + 1); + } + + // Next time the interception is not shown again. + MaybeIntercept(account_info.account_id); + EXPECT_EQ(interceptor()->is_interception_in_progress(), false); + histogram_tester.ExpectBucketCount( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kAbortUserDeclinedProfileForAccount, + 1); + + // Another account can still be intercepted. + account_info.email = "oscar@example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, + account_info, primary_account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); + histogram_tester.ExpectBucketCount( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptEnterprise, + kMaxProfileCreationDeclinedCount + 1); + EXPECT_EQ(interceptor()->is_interception_in_progress(), true); +} + +TEST_F(DiceWebSigninInterceptorTest, DeclineSwitchRepeatedly_NoLimit) { + base::HistogramTester histogram_tester; + // Setup for profile switch interception. + std::string email = "bob@example.com"; + AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + + // Test that the profile switch can be declined multiple times. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, + account_info, AccountInfo(), SkColor()}; + for (int i = 0; i < 10; ++i) { + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)) + .WillOnce(testing::WithArg<2>(testing::Invoke( + [](base::OnceCallback<void(SigninInterceptionResult)> callback) { + std::move(callback).Run(SigninInterceptionResult::kDeclined); + return nullptr; + }))); + MaybeIntercept(account_info.account_id); + EXPECT_EQ(interceptor()->is_interception_in_progress(), false); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptProfileSwitch, i + 1); + } +} + +TEST_F(DiceWebSigninInterceptorTest, PersistentHash) { + // The hash is persistent (the value should never change). + EXPECT_EQ("email_174", + interceptor()->GetPersistentEmailHash("alice@example.com")); + // Different email get another hash. + EXPECT_NE(interceptor()->GetPersistentEmailHash("bob@gmail.com"), + interceptor()->GetPersistentEmailHash("alice@example.com")); + // Equivalent emails get the same hash. + EXPECT_EQ(interceptor()->GetPersistentEmailHash("bob"), + interceptor()->GetPersistentEmailHash("bob@gmail.com")); + EXPECT_EQ(interceptor()->GetPersistentEmailHash("bo.b@gmail.com"), + interceptor()->GetPersistentEmailHash("bob@gmail.com")); + // Dots are removed only for gmail accounts. + EXPECT_NE(interceptor()->GetPersistentEmailHash("alice@example.com"), + interceptor()->GetPersistentEmailHash("al.ice@example.com")); +} + +// Interception other than the profile switch require at least 2 accounts. +TEST_F(DiceWebSigninInterceptorTest, NoInterceptionWithOneAccount) { + base::HistogramTester histogram_tester; + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("bob@example.com"); + // Interception aborts even if the account info is not available. + ASSERT_FALSE(identity_test_env() + ->identity_manager() + ->FindExtendedAccountInfoByAccountId(account_info.account_id) + .IsValid()); + TestSynchronousInterception( + account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kAbortSingleAccount); +} + +// When profile creation is disallowed, profile switch interception is still +// enabled, but others are disabled. +TEST_F(DiceWebSigninInterceptorTest, ProfileCreationDisallowed) { + base::HistogramTester histogram_tester; + g_browser_process->local_state()->SetBoolean(prefs::kBrowserAddPersonEnabled, + false); + // Setup for profile switch interception. + std::string email = "bob@example.com"; + AccountInfo account_info = identity_test_env()->MakeAccountAvailable(email); + AccountInfo other_account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + Profile* profile_2 = CreateTestingProfile("Profile 2"); + ProfileAttributesEntry* entry = + profile_attributes_storage()->GetProfileAttributesWithPath( + profile_2->GetPath()); + ASSERT_NE(entry, nullptr); + entry->SetAuthInfo(account_info.gaia, base::UTF8ToUTF16(email), + /*is_consented_primary_account=*/false); + + // Interception that would offer creating a new profile does not work. + TestSynchronousInterception( + other_account_info, /*is_new_account=*/true, /*is_sync_signin=*/false, + SigninInterceptionHeuristicOutcome::kAbortProfileCreationDisallowed); + + // Profile switch interception still works. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kProfileSwitch, + account_info, AccountInfo(), SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); +} + +TEST_F(DiceWebSigninInterceptorTest, WaitForAccountInfoAvailable) { + base::HistogramTester histogram_tester; + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "bob@example.com", signin::ConsentLevel::kSignin); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + EXPECT_FALSE(interceptor() + ->GetHeuristicOutcome(/*is_new_account=*/true, + /*is_sync_signin=*/false, + account_info.email, + /*entry=*/nullptr) + .has_value()); + MaybeIntercept(account_info.account_id); + // Delegate was not called yet. + testing::Mock::VerifyAndClearExpectations(mock_delegate()); + + // Account info becomes available, interception happens. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, + account_info, primary_account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration", + 1); +} + +TEST_F(DiceWebSigninInterceptorTest, AccountInfoAlreadyAvailable) { + base::HistogramTester histogram_tester; + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "bob@example.com", signin::ConsentLevel::kSignin); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + account_info.hosted_domain = "example.com"; + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Account info is already available, interception happens immediately. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kEnterprise, + account_info, primary_account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); + histogram_tester.ExpectTotalCount("Signin.Intercept.AccountInfoFetchDuration", + 1); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptEnterprise, 1); +} + +TEST_F(DiceWebSigninInterceptorTest, MultiUserInterception) { + base::HistogramTester histogram_tester; + AccountInfo primary_account_info = + identity_test_env()->MakePrimaryAccountAvailable( + "bob@example.com", signin::ConsentLevel::kSignin); + AccountInfo account_info = + identity_test_env()->MakeAccountAvailable("alice@example.com"); + MakeValidAccountInfo(&account_info); + identity_test_env()->UpdateAccountInfoForAccount(account_info); + + // Account info is already available, interception happens immediately. + DiceWebSigninInterceptor::Delegate::BubbleParameters expected_parameters = { + DiceWebSigninInterceptor::SigninInterceptionType::kMultiUser, + account_info, primary_account_info, SkColor()}; + EXPECT_CALL(*mock_delegate(), + ShowSigninInterceptionBubble( + web_contents(), MatchBubbleParameters(expected_parameters), + testing::_)); + MaybeIntercept(account_info.account_id); + histogram_tester.ExpectUniqueSample( + "Signin.Intercept.HeuristicOutcome", + SigninInterceptionHeuristicOutcome::kInterceptMultiUser, 1); +} diff --git a/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc b/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc new file mode 100644 index 00000000000..3e042e76b87 --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/live_sign_in_test.cc @@ -0,0 +1,776 @@ +// 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. + +#include "base/memory/raw_ptr.h" +#include "base/run_loop.h" +#include "base/scoped_observation.h" +#include "base/strings/stringprintf.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/account_reconcilor_factory.h" +#include "chrome/browser/signin/e2e_tests/live_test.h" +#include "chrome/browser/signin/e2e_tests/test_accounts_util.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/sync/sync_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/tabs/tab_strip_model_observer.h" +#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h" +#include "chrome/browser/ui/webui/signin/signin_email_confirmation_dialog.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/signin/core/browser/account_reconcilor.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/identity_manager/account_capabilities.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/signin/public/identity_manager/tribool.h" +#include "components/sync/driver/sync_service.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/core_account_id.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "google_apis/gaia/gaia_urls.h" +#include "ui/compositor/scoped_animation_duration_scale_mode.h" + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/sync/sync_ui_util.h" +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +namespace signin { +namespace test { + +const base::TimeDelta kDialogTimeout = base::Seconds(10); + +// A wrapper importing the settings module when the chrome://settings serve the +// Polymer 3 version. +const char kSettingsScriptWrapperFormat[] = + "import('./settings.js').then(settings => {%s});"; + +enum class PrimarySyncAccountWait { kWaitForAdded, kWaitForCleared, kNotWait }; + +// Observes various sign-in events and allows to wait for a specific state of +// signed-in accounts. +class SignInTestObserver : public IdentityManager::Observer, + public AccountReconcilor::Observer { + public: + explicit SignInTestObserver(IdentityManager* identity_manager, + AccountReconcilor* reconcilor) + : identity_manager_(identity_manager), reconcilor_(reconcilor) { + identity_manager_observation_.Observe(identity_manager_.get()); + account_reconcilor_observation_.Observe(reconcilor_.get()); + } + ~SignInTestObserver() override = default; + + // IdentityManager::Observer: + void OnPrimaryAccountChanged( + const PrimaryAccountChangeEvent& event) override { + if (event.GetEventTypeFor(ConsentLevel::kSync) == + PrimaryAccountChangeEvent::Type::kNone) { + return; + } + QuitIfConditionIsSatisfied(); + } + void OnRefreshTokenUpdatedForAccount(const CoreAccountInfo&) override { + QuitIfConditionIsSatisfied(); + } + void OnRefreshTokenRemovedForAccount(const CoreAccountId&) override { + QuitIfConditionIsSatisfied(); + } + void OnErrorStateOfRefreshTokenUpdatedForAccount( + const CoreAccountInfo&, + const GoogleServiceAuthError&) override { + QuitIfConditionIsSatisfied(); + } + void OnAccountsInCookieUpdated(const AccountsInCookieJarInfo&, + const GoogleServiceAuthError&) override { + QuitIfConditionIsSatisfied(); + } + + // AccountReconcilor::Observer: + // TODO(https://crbug.com/1051864): Remove this obsever method once the bug is + // fixed. + void OnStateChanged(signin_metrics::AccountReconcilorState state) override { + if (state == signin_metrics::ACCOUNT_RECONCILOR_OK) { + // This will trigger cookie update if accounts are stale. + identity_manager_->GetAccountsInCookieJar(); + } + } + + void WaitForAccountChanges(int signed_in_accounts, + PrimarySyncAccountWait primary_sync_account_wait) { + expected_signed_in_accounts_ = signed_in_accounts; + primary_sync_account_wait_ = primary_sync_account_wait; + are_expectations_set = true; + QuitIfConditionIsSatisfied(); + run_loop_.Run(); + } + + private: + void QuitIfConditionIsSatisfied() { + if (!are_expectations_set) + return; + + int accounts_with_valid_refresh_token = + CountAccountsWithValidRefreshToken(); + int accounts_in_cookie = CountSignedInAccountsInCookie(); + + if (accounts_with_valid_refresh_token != accounts_in_cookie || + accounts_with_valid_refresh_token != expected_signed_in_accounts_) { + return; + } + + switch (primary_sync_account_wait_) { + case PrimarySyncAccountWait::kWaitForAdded: + if (!HasValidPrimarySyncAccount()) + return; + break; + case PrimarySyncAccountWait::kWaitForCleared: + if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) + return; + break; + case PrimarySyncAccountWait::kNotWait: + break; + } + + run_loop_.Quit(); + } + + int CountAccountsWithValidRefreshToken() const { + std::vector<CoreAccountInfo> accounts_with_refresh_tokens = + identity_manager_->GetAccountsWithRefreshTokens(); + int valid_accounts = 0; + for (const auto& account_info : accounts_with_refresh_tokens) { + if (!identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id)) { + ++valid_accounts; + } + } + return valid_accounts; + } + + int CountSignedInAccountsInCookie() const { + signin::AccountsInCookieJarInfo accounts_in_cookie_jar = + identity_manager_->GetAccountsInCookieJar(); + if (!accounts_in_cookie_jar.accounts_are_fresh) + return -1; + + return accounts_in_cookie_jar.signed_in_accounts.size(); + } + + bool HasValidPrimarySyncAccount() const { + CoreAccountId primary_account_id = + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync); + if (primary_account_id.empty()) + return false; + + return !identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState( + primary_account_id); + } + + const raw_ptr<signin::IdentityManager> identity_manager_; + const raw_ptr<AccountReconcilor> reconcilor_; + base::ScopedObservation<IdentityManager, IdentityManager::Observer> + identity_manager_observation_{this}; + base::ScopedObservation<AccountReconcilor, AccountReconcilor::Observer> + account_reconcilor_observation_{this}; + base::RunLoop run_loop_; + + bool are_expectations_set = false; + int expected_signed_in_accounts_ = 0; + PrimarySyncAccountWait primary_sync_account_wait_ = + PrimarySyncAccountWait::kNotWait; +}; + +// Observer class allowing to wait for account capabilities to be known. +class AccountCapabilitiesObserver : public IdentityManager::Observer { + public: + explicit AccountCapabilitiesObserver(IdentityManager* identity_manager) + : identity_manager_(identity_manager) { + identity_manager_observation_.Observe(identity_manager); + } + + // IdentityManager::Observer: + void OnExtendedAccountInfoUpdated(const AccountInfo& info) override { + if (info.account_id != account_id_) + return; + + if (info.capabilities.AreAllCapabilitiesKnown()) + run_loop_.Quit(); + } + + // This should be called only once per AccountCapabilitiesObserver instance. + void WaitForAllCapabilitiesToBeKnown(CoreAccountId account_id) { + DCHECK(identity_manager_observation_.IsObservingSource( + identity_manager_.get())); + AccountInfo info = + identity_manager_->FindExtendedAccountInfoByAccountId(account_id); + if (info.capabilities.AreAllCapabilitiesKnown()) + return; + + account_id_ = account_id; + run_loop_.Run(); + identity_manager_observation_.Reset(); + } + + private: + raw_ptr<IdentityManager> identity_manager_ = nullptr; + CoreAccountId account_id_; + base::RunLoop run_loop_; + base::ScopedObservation<IdentityManager, IdentityManager::Observer> + identity_manager_observation_{this}; +}; + +// Live tests for SignIn. +// These tests can be run with: +// browser_tests --gtest_filter=LiveSignInTest.* --run-live-tests --run-manual +class LiveSignInTest : public signin::test::LiveTest { + public: + LiveSignInTest() = default; + ~LiveSignInTest() override = default; + + void SetUp() override { + LiveTest::SetUp(); + // Always disable animation for stability. + ui::ScopedAnimationDurationScaleMode disable_animation( + ui::ScopedAnimationDurationScaleMode::ZERO_DURATION); + } + + void SignInFromWeb(const TestAccount& test_account, + int previously_signed_in_accounts) { + AddTabAtIndex(0, GaiaUrls::GetInstance()->add_account_url(), + ui::PageTransition::PAGE_TRANSITION_TYPED); + SignInFromCurrentPage(test_account, previously_signed_in_accounts); + } + + void SignInFromSettings(const TestAccount& test_account, + int previously_signed_in_accounts) { + GURL settings_url("chrome://settings"); + AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED); + auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents(); + EXPECT_TRUE(content::ExecuteScript( + settings_tab, + base::StringPrintf( + kSettingsScriptWrapperFormat, + "settings.SyncBrowserProxyImpl.getInstance().startSignIn();"))); + SignInFromCurrentPage(test_account, previously_signed_in_accounts); + } + + void SignInFromCurrentPage(const TestAccount& test_account, + int previously_signed_in_accounts) { + SignInTestObserver observer(identity_manager(), account_reconcilor()); + login_ui_test_utils::ExecuteJsToSigninInSigninFrame( + browser(), test_account.user, test_account.password); + observer.WaitForAccountChanges(previously_signed_in_accounts + 1, + PrimarySyncAccountWait::kNotWait); + } + + void TurnOnSync(const TestAccount& test_account, + int previously_signed_in_accounts) { + SignInFromSettings(test_account, previously_signed_in_accounts); + + SignInTestObserver observer(identity_manager(), account_reconcilor()); + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog( + browser(), kDialogTimeout)); + observer.WaitForAccountChanges(previously_signed_in_accounts + 1, + PrimarySyncAccountWait::kWaitForAdded); + } + + void SignOutFromWeb() { + SignInTestObserver observer(identity_manager(), account_reconcilor()); + AddTabAtIndex(0, GaiaUrls::GetInstance()->service_logout_url(), + ui::PageTransition::PAGE_TRANSITION_TYPED); + observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kNotWait); + } + + void TurnOffSync() { + GURL settings_url("chrome://settings"); + AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED); + SignInTestObserver observer(identity_manager(), account_reconcilor()); + auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents(); + EXPECT_TRUE(content::ExecuteScript( + settings_tab, + base::StringPrintf( + kSettingsScriptWrapperFormat, + "settings.SyncBrowserProxyImpl.getInstance().signOut(false)"))); + observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared); + } + + signin::IdentityManager* identity_manager() { + return identity_manager(browser()); + } + + signin::IdentityManager* identity_manager(Browser* browser) { + return IdentityManagerFactory::GetForProfile(browser->profile()); + } + + syncer::SyncService* sync_service() { return sync_service(browser()); } + + syncer::SyncService* sync_service(Browser* browser) { + return SyncServiceFactory::GetForProfile(browser->profile()); + } + + AccountReconcilor* account_reconcilor() { + return account_reconcilor(browser()); + } + + AccountReconcilor* account_reconcilor(Browser* browser) { + return AccountReconcilorFactory::GetForProfile(browser->profile()); + } +}; + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Sings in an account through the settings page and checks that the account is +// added to Chrome. Sync should be disabled because the test doesn't pass +// through the Sync confirmation dialog. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_SimpleSignInFlow) { + TestAccount ta; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", ta)); + SignInFromSettings(ta, 0); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size()); + EXPECT_TRUE(accounts_in_cookie_jar.signed_out_accounts.empty()); + const gaia::ListedAccount& account = + accounts_in_cookie_jar.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(ta.user, account.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account.id)); + EXPECT_FALSE(sync_service()->IsSyncFeatureEnabled()); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Signs in an account through the settings page and enables Sync. Checks that +// Sync is enabled. +// Then, signs out on the web and checks that the account is removed from +// cookies and Sync paused error is displayed. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_WebSignOut) { + TestAccount test_account; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account)); + TurnOnSync(test_account, 0); + + const CoreAccountInfo& primary_account = + identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account.user, primary_account.email)); + EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled()); + + SignOutFromWeb(); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty()); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_out_accounts.size()); + EXPECT_TRUE(gaia::AreEmailsSame( + test_account.user, accounts_in_cookie_jar.signed_out_accounts[0].email)); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + primary_account.account_id)); +#if !BUILDFLAG(IS_CHROMEOS_ASH) + EXPECT_EQ(GetAvatarSyncErrorType(browser()->profile()), + AvatarSyncErrorType::kAuthError); +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Sings in two accounts on the web and checks that cookies and refresh tokens +// are added to Chrome. Sync should be disabled. +// Then, signs out on the web and checks that accounts are removed from Chrome. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_WebSignInAndSignOut) { + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + SignInFromWeb(test_account_1, 0); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar_1 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_1.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar_1.signed_in_accounts.size()); + EXPECT_TRUE(accounts_in_cookie_jar_1.signed_out_accounts.empty()); + const gaia::ListedAccount& account_1 = + accounts_in_cookie_jar_1.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, account_1.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_1.id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromWeb(test_account_2, 1); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh); + ASSERT_EQ(2u, accounts_in_cookie_jar_2.signed_in_accounts.size()); + EXPECT_TRUE(accounts_in_cookie_jar_2.signed_out_accounts.empty()); + EXPECT_EQ(accounts_in_cookie_jar_2.signed_in_accounts[0].id, account_1.id); + const gaia::ListedAccount& account_2 = + accounts_in_cookie_jar_2.signed_in_accounts[1]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account_2.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account_2.id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + + SignOutFromWeb(); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar_3 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_3.accounts_are_fresh); + ASSERT_TRUE(accounts_in_cookie_jar_3.signed_in_accounts.empty()); + EXPECT_EQ(2u, accounts_in_cookie_jar_3.signed_out_accounts.size()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Signs in an account through the settings page and enables Sync. Checks that +// Sync is enabled. Signs in a second account on the web. +// Then, turns Sync off from the settings page and checks that both accounts are +// removed from Chrome and from cookies. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_TurnOffSync) { + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromWeb(test_account_2, 1); + + const CoreAccountInfo& primary_account = + identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, primary_account.email)); + EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled()); + + TurnOffSync(); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh); + ASSERT_TRUE(accounts_in_cookie_jar_2.signed_in_accounts.empty()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// In "Sync paused" state, when the primary account is invalid, turns off sync +// from settings. Checks that the account is removed from Chrome. +// Regression test for https://crbug.com/1114646 +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_TurnOffSyncWhenPaused) { + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + + // Get in sync paused state. + SignOutFromWeb(); + + const CoreAccountInfo& primary_account = + identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account_1.user, primary_account.email)); + EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled()); + EXPECT_TRUE( + identity_manager()->HasAccountWithRefreshTokenInPersistentErrorState( + primary_account.account_id)); + + TurnOffSync(); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Signs in an account on the web. Goes to the Chrome settings to enable Sync +// but cancels the sync confirmation dialog. Checks that the account is still +// signed in on the web but Sync is disabled. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_CancelSyncWithWebAccount) { + TestAccount test_account; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account)); + SignInFromWeb(test_account, 0); + + SignInTestObserver observer(identity_manager(), account_reconcilor()); + GURL settings_url("chrome://settings"); + AddTabAtIndex(0, settings_url, ui::PageTransition::PAGE_TRANSITION_TYPED); + auto* settings_tab = browser()->tab_strip_model()->GetActiveWebContents(); + std::string start_syncing_script = base::StringPrintf( + "settings.SyncBrowserProxyImpl.getInstance()." + "startSyncingWithEmail(\"%s\", true);", + test_account.user.c_str()); + EXPECT_TRUE(content::ExecuteScript( + settings_tab, base::StringPrintf(kSettingsScriptWrapperFormat, + start_syncing_script.c_str()))); + EXPECT_TRUE(login_ui_test_utils::CancelSyncConfirmationDialog( + browser(), kDialogTimeout)); + observer.WaitForAccountChanges(1, PrimarySyncAccountWait::kWaitForCleared); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size()); + const gaia::ListedAccount& account = + accounts_in_cookie_jar.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account.user, account.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken(account.id)); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Starts the sign in flow from the settings page, enters credentials on the +// login page but cancels the Sync confirmation dialog. Checks that Sync is +// disabled and no account was added to Chrome. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, MANUAL_CancelSync) { + TestAccount test_account; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account)); + SignInFromSettings(test_account, 0); + + SignInTestObserver observer(identity_manager(), account_reconcilor()); + EXPECT_TRUE(login_ui_test_utils::CancelSyncConfirmationDialog( + browser(), kDialogTimeout)); + observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared); + + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + EXPECT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Enables and disables sync to account 1. Enables sync to account 2 and clicks +// on "This wasn't me" in the email confirmation dialog. Checks that the new +// profile is created. Checks that Sync to account 2 is enabled in the new +// profile. Checks that account 2 was removed from the original profile. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, + MANUAL_SyncSecondAccount_CreateNewProfile) { + // Enable and disable sync for the first account. + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + TurnOffSync(); + + // Start enable sync for the second account. + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromSettings(test_account_2, 0); + + // Set up an observer for removing the second account from the original + // profile. + SignInTestObserver original_browser_observer(identity_manager(), + account_reconcilor()); + + // Check there is only one profile. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Click "This wasn't me" on the email confirmation dialog and wait for a new + // browser and profile created. + EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog( + browser(), kDialogTimeout, + SigninEmailConfirmationDialog::CREATE_NEW_USER)); + Browser* new_browser = ui_test_utils::WaitForBrowserToOpen(); + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 2U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 2U); + EXPECT_NE(browser()->profile(), new_browser->profile()); + + // Confirm sync in the new browser window. + SignInTestObserver new_browser_observer(identity_manager(new_browser), + account_reconcilor(new_browser)); + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog( + new_browser, kDialogTimeout)); + new_browser_observer.WaitForAccountChanges( + 1, PrimarySyncAccountWait::kWaitForAdded); + + // Check accounts in cookies in the new profile. + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager(new_browser)->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size()); + const gaia::ListedAccount& account = + accounts_in_cookie_jar.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account.email)); + + // Check the primary account in the new profile is set and syncing. + const CoreAccountInfo& primary_account = + identity_manager(new_browser) + ->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, primary_account.email)); + EXPECT_TRUE(identity_manager(new_browser) + ->HasAccountWithRefreshToken(primary_account.account_id)); + EXPECT_TRUE(sync_service(new_browser)->IsSyncFeatureEnabled()); + + // Check that the second account was removed from the original profile. + original_browser_observer.WaitForAccountChanges( + 0, PrimarySyncAccountWait::kWaitForCleared); + const AccountsInCookieJarInfo& accounts_in_cookie_jar_2 = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar_2.accounts_are_fresh); + ASSERT_TRUE(accounts_in_cookie_jar_2.signed_in_accounts.empty()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Enables and disables sync to account 1. Enables sync to account 2 and clicks +// on "This was me" in the email confirmation dialog. Checks that Sync to +// account 2 is enabled in the current profile. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, + MANUAL_SyncSecondAccount_InExistingProfile) { + // Enable and disable sync for the first account. + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + TurnOffSync(); + + // Start enable sync for the second account. + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromSettings(test_account_2, 0); + + // Check there is only one profile. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Click "This was me" on the email confirmation dialog, confirm sync and wait + // for a primary account to be set. + SignInTestObserver observer(identity_manager(), account_reconcilor()); + EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog( + browser(), kDialogTimeout, SigninEmailConfirmationDialog::START_SYNC)); + EXPECT_TRUE(login_ui_test_utils::ConfirmSyncConfirmationDialog( + browser(), kDialogTimeout)); + observer.WaitForAccountChanges(1, PrimarySyncAccountWait::kWaitForAdded); + + // Check no profile was created. + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Check accounts in cookies. + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + ASSERT_EQ(1u, accounts_in_cookie_jar.signed_in_accounts.size()); + const gaia::ListedAccount& account = + accounts_in_cookie_jar.signed_in_accounts[0]; + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, account.email)); + + // Check the primary account is set and syncing. + const CoreAccountInfo& primary_account = + identity_manager()->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + EXPECT_FALSE(primary_account.IsEmpty()); + EXPECT_TRUE(gaia::AreEmailsSame(test_account_2.user, primary_account.email)); + EXPECT_TRUE(identity_manager()->HasAccountWithRefreshToken( + primary_account.account_id)); + EXPECT_TRUE(sync_service()->IsSyncFeatureEnabled()); +} + +// This test can pass. Marked as manual because it TIMED_OUT on Win7. +// See crbug.com/1025335. +// Enables and disables sync to account 1. Enables sync to account 2 and clicks +// on "Cancel" in the email confirmation dialog. Checks that the signin flow is +// canceled and no accounts are added to Chrome. +IN_PROC_BROWSER_TEST_F(LiveSignInTest, + MANUAL_SyncSecondAccount_CancelOnEmailConfirmation) { + // Enable and disable sync for the first account. + TestAccount test_account_1; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", test_account_1)); + TurnOnSync(test_account_1, 0); + TurnOffSync(); + + // Start enable sync for the second account. + TestAccount test_account_2; + CHECK(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_2", test_account_2)); + SignInFromSettings(test_account_2, 0); + + // Check there is only one profile. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Click "Cancel" on the email confirmation dialog and wait for an account to + // removed from Chrome. + SignInTestObserver observer(identity_manager(), account_reconcilor()); + EXPECT_TRUE(login_ui_test_utils::CompleteSigninEmailConfirmationDialog( + browser(), kDialogTimeout, SigninEmailConfirmationDialog::CLOSE)); + observer.WaitForAccountChanges(0, PrimarySyncAccountWait::kWaitForCleared); + + // Check no profile was created. + EXPECT_EQ(profile_manager->GetNumberOfProfiles(), 1U); + EXPECT_EQ(chrome::GetTotalBrowserCount(), 1U); + + // Check Chrome has no accounts. + const AccountsInCookieJarInfo& accounts_in_cookie_jar = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(accounts_in_cookie_jar.accounts_are_fresh); + EXPECT_TRUE(accounts_in_cookie_jar.signed_in_accounts.empty()); + EXPECT_TRUE(identity_manager()->GetAccountsWithRefreshTokens().empty()); + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); +} + +IN_PROC_BROWSER_TEST_F(LiveSignInTest, + MANUAL_AccountCapabilities_FetchedOnSignIn) { + EnableAccountCapabilitiesFetches(identity_manager()); + + // Test primary adult account. + { + AccountCapabilitiesObserver capabilities_observer(identity_manager()); + + TestAccount ta; + ASSERT_TRUE(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_1", ta)); + SignInFromSettings(ta, 0); + + CoreAccountInfo core_account_info = + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin); + ASSERT_TRUE(gaia::AreEmailsSame(core_account_info.email, ta.user)); + + capabilities_observer.WaitForAllCapabilitiesToBeKnown( + core_account_info.account_id); + AccountInfo account_info = + identity_manager()->FindExtendedAccountInfoByAccountId( + core_account_info.account_id); + EXPECT_EQ(account_info.capabilities.can_offer_extended_chrome_sync_promos(), + Tribool::kTrue); + } + + // Test secondary minor account. + { + AccountCapabilitiesObserver capabilities_observer(identity_manager()); + + TestAccount ta; + ASSERT_TRUE(GetTestAccountsUtil()->GetAccount("TEST_ACCOUNT_MINOR", ta)); + SignInFromWeb(ta, /*previously_signed_in_accounts=*/1); + + CoreAccountInfo core_account_info = + identity_manager()->FindExtendedAccountInfoByEmailAddress(ta.user); + ASSERT_FALSE(core_account_info.IsEmpty()); + + capabilities_observer.WaitForAllCapabilitiesToBeKnown( + core_account_info.account_id); + AccountInfo account_info = + identity_manager()->FindExtendedAccountInfoByAccountId( + core_account_info.account_id); + EXPECT_EQ(account_info.capabilities.can_offer_extended_chrome_sync_promos(), + Tribool::kFalse); + } +} + +} // namespace test +} // namespace signin diff --git a/chromium/chrome/browser/signin/e2e_tests/live_test.cc b/chromium/chrome/browser/signin/e2e_tests/live_test.cc new file mode 100644 index 00000000000..e8d22189e73 --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/live_test.cc @@ -0,0 +1,61 @@ +// 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. +#include "chrome/browser/signin/e2e_tests/live_test.h" + +#include "base/files/file_util.h" +#include "base/path_service.h" +#include "net/dns/mock_host_resolver.h" + +base::FilePath::StringPieceType kTestAccountFilePath = FILE_PATH_LITERAL( + "chrome/browser/internal/resources/signin/test_accounts.json"); + +const char* kRunLiveTestFlag = "run-live-tests"; + +namespace signin { +namespace test { + +void LiveTest::SetUpInProcessBrowserTestFixture() { + // Whitelists a bunch of hosts. + host_resolver()->AllowDirectLookup("*.google.com"); + host_resolver()->AllowDirectLookup("*.geotrust.com"); + host_resolver()->AllowDirectLookup("*.gstatic.com"); + host_resolver()->AllowDirectLookup("*.googleapis.com"); + // Allows country-specific TLDs. + host_resolver()->AllowDirectLookup("accounts.google.*"); + + InProcessBrowserTest::SetUpInProcessBrowserTestFixture(); +} + +void LiveTest::SetUp() { + // Only run live tests when specified. + auto* cmd_line = base::CommandLine::ForCurrentProcess(); + if (!cmd_line->HasSwitch(kRunLiveTestFlag)) { + LOG(INFO) << "This test should get skipped."; + skip_test_ = true; + GTEST_SKIP(); + } + base::FilePath root_path; + base::PathService::Get(base::BasePathKey::DIR_SOURCE_ROOT, &root_path); + base::FilePath config_path = + base::MakeAbsoluteFilePath(root_path.Append(kTestAccountFilePath)); + test_accounts_.Init(config_path); + InProcessBrowserTest::SetUp(); +} + +void LiveTest::TearDown() { + // This test was skipped, no need to tear down. + if (skip_test_) + return; + InProcessBrowserTest::TearDown(); +} + +void LiveTest::PostRunTestOnMainThread() { + // This test was skipped. Running PostRunTestOnMainThread can cause + // TIMED_OUT on Win7. + if (skip_test_) + return; + InProcessBrowserTest::PostRunTestOnMainThread(); +} +} // namespace test +} // namespace signin diff --git a/chromium/chrome/browser/signin/e2e_tests/live_test.h b/chromium/chrome/browser/signin/e2e_tests/live_test.h new file mode 100644 index 00000000000..6de9f8f4ded --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/live_test.h @@ -0,0 +1,33 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_ +#define CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_ + +#include "chrome/browser/signin/e2e_tests/test_accounts_util.h" +#include "chrome/test/base/in_process_browser_test.h" + +namespace signin { +namespace test { + +class LiveTest : public InProcessBrowserTest { + protected: + void SetUpInProcessBrowserTestFixture() override; + void SetUp() override; + void TearDown() override; + void PostRunTestOnMainThread() override; + + const TestAccountsUtil* GetTestAccountsUtil() const { + return &test_accounts_; + } + + private: + TestAccountsUtil test_accounts_; + bool skip_test_ = false; +}; + +} // namespace test +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_E2E_TESTS_LIVE_TEST_H_ diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc new file mode 100644 index 00000000000..c533ea0fb3c --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.cc @@ -0,0 +1,75 @@ +// 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. + +#include "chrome/browser/signin/e2e_tests/test_accounts_util.h" + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/json/json_file_value_serializer.h" +#include "base/json/json_reader.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" + +using base::Value; + +namespace signin { +namespace test { + +#if defined(OS_WIN) +std::string kPlatform = "win"; +#elif defined(OS_MAC) +std::string kPlatform = "mac"; +#elif BUILDFLAG(IS_CHROMEOS_ASH) +std::string kPlatform = "chromeos"; +#elif defined(OS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) +std::string kPlatform = "linux"; +#elif defined(OS_ANDROID) +std::string kPlatform = "android"; +#else +std::string kPlatform = "all_platform"; +#endif + +TestAccountsUtil::TestAccountsUtil() = default; +TestAccountsUtil::~TestAccountsUtil() = default; + +bool TestAccountsUtil::Init(const base::FilePath& config_path) { + int error_code = 0; + std::string error_str; + JSONFileValueDeserializer deserializer(config_path); + std::unique_ptr<Value> content_json = + deserializer.Deserialize(&error_code, &error_str); + CHECK(error_code == 0) << "Error reading json file. Error code: " + << error_code << " " << error_str; + CHECK(content_json); + + // Only store platform specific users. If an account does not have + // platform specific user, try to use all_platform user. + for (auto account : content_json->DictItems()) { + const Value* platform_account = account.second.FindDictKey(kPlatform); + if (platform_account == nullptr) { + platform_account = account.second.FindDictKey("all_platform"); + if (platform_account == nullptr) { + continue; + } + } + TestAccount ta(*(platform_account->FindStringKey("user")), + *(platform_account->FindStringKey("password"))); + all_accounts_.insert( + std::pair<std::string, TestAccount>(account.first, ta)); + } + return true; +} + +bool TestAccountsUtil::GetAccount(const std::string& name, + TestAccount& out_account) const { + auto it = all_accounts_.find(name); + if (it == all_accounts_.end()) { + return false; + } + out_account = it->second; + return true; +} + +} // namespace test +} // namespace signin diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h new file mode 100644 index 00000000000..4f73dd76d32 --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util.h @@ -0,0 +1,46 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_ + +#include <map> +#include <string> + +namespace base { +class FilePath; +} + +namespace signin { +namespace test { + +struct TestAccount { + std::string user; + std::string password; + TestAccount() = default; + TestAccount(const std::string& user, const std::string& password) { + this->user = user; + this->password = password; + } +}; + +class TestAccountsUtil { + public: + TestAccountsUtil(); + + TestAccountsUtil(const TestAccountsUtil&) = delete; + TestAccountsUtil& operator=(const TestAccountsUtil&) = delete; + + virtual ~TestAccountsUtil(); + bool Init(const base::FilePath& config_path); + bool GetAccount(const std::string& name, TestAccount& out_account) const; + + private: + std::map<std::string, TestAccount> all_accounts_; +}; + +} // namespace test +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_E2E_TESTS_TEST_ACCOUNTS_UTIL_H_ diff --git a/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc new file mode 100644 index 00000000000..7b63dd6128a --- /dev/null +++ b/chromium/chrome/browser/signin/e2e_tests/test_accounts_util_unittest.cc @@ -0,0 +1,100 @@ +// 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. + +#include "chrome/browser/signin/e2e_tests/test_accounts_util.h" +#include "base/files/file_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::FilePath; + +namespace signin { +namespace test { + +class TestAccountsUtilTest : public testing::Test {}; + +FilePath WriteContentToTemporaryFile(const char* contents, + unsigned int length) { + FilePath tmp_file; + CHECK(base::CreateTemporaryFile(&tmp_file)); + unsigned int bytes_written = base::WriteFile(tmp_file, contents, length); + CHECK_EQ(bytes_written, length); + return tmp_file; +} + +TEST(TestAccountsUtilTest, ParsingJson) { + const char contents[] = + "{ \n" + " \"TEST_ACCOUNT_1\": {\n" + " \"win\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " }\n" + " }\n" + "}"; + FilePath tmp_file = + WriteContentToTemporaryFile(contents, sizeof(contents) - 1); + TestAccountsUtil util; + util.Init(tmp_file); +} + +TEST(TestAccountsUtilTest, GetAccountForPlatformSpecific) { + const char contents[] = + "{ \n" + " \"TEST_ACCOUNT_1\": {\n" + " \"win\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " },\n" + " \"mac\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " },\n" + " \"linux\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " },\n" + " \"chromeos\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " },\n" + " \"android\": {\n" + " \"user\": \"user1\",\n" + " \"password\": \"pwd1\"\n" + " }\n" + " }\n" + "}"; + FilePath tmp_file = + WriteContentToTemporaryFile(contents, sizeof(contents) - 1); + TestAccountsUtil util; + util.Init(tmp_file); + TestAccount ta; + bool ret = util.GetAccount("TEST_ACCOUNT_1", ta); + ASSERT_TRUE(ret); + ASSERT_EQ(ta.user, "user1"); + ASSERT_EQ(ta.password, "pwd1"); +} + +TEST(TestAccountsUtilTest, GetAccountForAllPlatform) { + const char contents[] = + "{ \n" + " \"TEST_ACCOUNT_1\": {\n" + " \"all_platform\": {\n" + " \"user\": \"user_allplatform\",\n" + " \"password\": \"pwd_allplatform\"\n" + " }\n" + " }\n" + "}"; + FilePath tmp_file = + WriteContentToTemporaryFile(contents, sizeof(contents) - 1); + TestAccountsUtil util; + util.Init(tmp_file); + TestAccount ta; + bool ret = util.GetAccount("TEST_ACCOUNT_1", ta); + ASSERT_TRUE(ret); + ASSERT_EQ(ta.user, "user_allplatform"); + ASSERT_EQ(ta.password, "pwd_allplatform"); +} + +} // namespace test +} // namespace signin diff --git a/chromium/chrome/browser/signin/force_signin_verifier.cc b/chromium/chrome/browser/signin/force_signin_verifier.cc new file mode 100644 index 00000000000..951efcd003c --- /dev/null +++ b/chromium/chrome/browser/signin/force_signin_verifier.cc @@ -0,0 +1,195 @@ +// Copyright 2017 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. + +#include <string> + +#include "chrome/browser/signin/force_signin_verifier.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/files/file_path.h" +#include "base/metrics/histogram_macros.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/profile_picker.h" +#include "chrome/browser/ui/ui_features.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/access_token_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "components/signin/public/identity_manager/scope_set.h" +#include "content/public/browser/network_service_instance.h" +#include "google_apis/gaia/gaia_constants.h" + +namespace { +const net::BackoffEntry::Policy kForceSigninVerifierBackoffPolicy = { + 0, // Number of initial errors to ignore before applying + // exponential back-off rules. + 2000, // Initial delay in ms. + 2, // Factor by which the waiting time will be multiplied. + 0.2, // Fuzzing percentage. + 4 * 60 * 1000, // Maximum amount of time to delay th request in ms. + -1, // Never discard the entry. + false // Do not always use initial delay. +}; + +} // namespace + +ForceSigninVerifier::ForceSigninVerifier( + Profile* profile, + signin::IdentityManager* identity_manager) + : has_token_verified_(false), + backoff_entry_(&kForceSigninVerifierBackoffPolicy), + creation_time_(base::TimeTicks::Now()), + profile_(profile), + identity_manager_(identity_manager) { + content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this); + // Most of time (~94%), sign-in token can be verified with server. + SendRequest(); +} + +ForceSigninVerifier::~ForceSigninVerifier() { + Cancel(); +} + +void ForceSigninVerifier::OnAccessTokenFetchComplete( + GoogleServiceAuthError error, + signin::AccessTokenInfo token_info) { + if (error.state() != GoogleServiceAuthError::NONE) { + if (error.IsPersistentError()) { + // Based on the obsolete UMA Signin.ForceSigninVerificationTime.Failure, + // about 7% verifications are failed. Most of them are finished within + // 113ms but some of them (<3%) could take longer than 3 minutes. + has_token_verified_ = true; + CloseAllBrowserWindows(); + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver( + this); + Cancel(); + } else { + backoff_entry_.InformOfRequest(false); + backoff_request_timer_.Start( + FROM_HERE, backoff_entry_.GetTimeUntilRelease(), + base::BindOnce(&ForceSigninVerifier::SendRequest, + weak_factory_.GetWeakPtr())); + access_token_fetcher_.reset(); + } + return; + } + + // Based on the obsolete UMA Signin.ForceSigninVerificationTime.Success, about + // 93% verifications are succeeded. Most of them are finished ~1 second but + // some of them (<3%) could take longer than 3 minutes. + has_token_verified_ = true; + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this); + Cancel(); +} + +void ForceSigninVerifier::OnConnectionChanged( + network::mojom::ConnectionType type) { + // Try again immediately once the network is back and cancel any pending + // request. + backoff_entry_.Reset(); + if (backoff_request_timer_.IsRunning()) + backoff_request_timer_.Stop(); + + SendRequestIfNetworkAvailable(type); +} + +void ForceSigninVerifier::Cancel() { + backoff_entry_.Reset(); + backoff_request_timer_.Stop(); + access_token_fetcher_.reset(); + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this); +} + +bool ForceSigninVerifier::HasTokenBeenVerified() { + return has_token_verified_; +} + +void ForceSigninVerifier::SendRequest() { + auto type = network::mojom::ConnectionType::CONNECTION_NONE; + if (content::GetNetworkConnectionTracker()->GetConnectionType( + &type, + base::BindOnce(&ForceSigninVerifier::SendRequestIfNetworkAvailable, + weak_factory_.GetWeakPtr()))) { + SendRequestIfNetworkAvailable(type); + } +} + +void ForceSigninVerifier::SendRequestIfNetworkAvailable( + network::mojom::ConnectionType network_type) { + if (network_type == network::mojom::ConnectionType::CONNECTION_NONE || + !ShouldSendRequest()) { + return; + } + + signin::ScopeSet oauth2_scopes; + oauth2_scopes.insert(GaiaConstants::kChromeSyncOAuth2Scope); + access_token_fetcher_ = + std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( + "force_signin_verifier", identity_manager_, oauth2_scopes, + base::BindOnce(&ForceSigninVerifier::OnAccessTokenFetchComplete, + weak_factory_.GetWeakPtr()), + signin::PrimaryAccountAccessTokenFetcher::Mode::kImmediate); +} + +bool ForceSigninVerifier::ShouldSendRequest() { + return !has_token_verified_ && access_token_fetcher_.get() == nullptr && + identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync); +} + +void ForceSigninVerifier::CloseAllBrowserWindows() { + if (base::FeatureList::IsEnabled(features::kForceSignInReauth)) { + // Do not sign the user out to allow them to reauthenticate from the profile + // picker. + BrowserList::CloseAllBrowsersWithProfile( + profile_, + base::BindRepeating(&ForceSigninVerifier::OnCloseBrowsersSuccess, + weak_factory_.GetWeakPtr()), + base::DoNothing(), + /*skip_beforeunload=*/true); + } else { + // Do not close window if there is ongoing reauth. If it fails later, the + // signin process should take care of the signout. + auto* primary_account_mutator = + identity_manager_->GetPrimaryAccountMutator(); + if (!primary_account_mutator) + return; + primary_account_mutator->ClearPrimaryAccount( + signin_metrics::AUTHENTICATION_FAILED_WITH_FORCE_SIGNIN, + signin_metrics::SignoutDelete::kIgnoreMetric); + } +} + +void ForceSigninVerifier::OnCloseBrowsersSuccess( + const base::FilePath& profile_path) { + Cancel(); + + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile_path); + if (!entry) + return; + entry->LockForceSigninProfile(true); + ProfilePicker::Show(ProfilePicker::EntryPoint::kProfileLocked); +} + +signin::PrimaryAccountAccessTokenFetcher* +ForceSigninVerifier::GetAccessTokenFetcherForTesting() { + return access_token_fetcher_.get(); +} + +net::BackoffEntry* ForceSigninVerifier::GetBackoffEntryForTesting() { + return &backoff_entry_; +} + +base::OneShotTimer* ForceSigninVerifier::GetOneShotTimerForTesting() { + return &backoff_request_timer_; +} diff --git a/chromium/chrome/browser/signin/force_signin_verifier.h b/chromium/chrome/browser/signin/force_signin_verifier.h new file mode 100644 index 00000000000..7260aa6f3fe --- /dev/null +++ b/chromium/chrome/browser/signin/force_signin_verifier.h @@ -0,0 +1,95 @@ +// Copyright 2017 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. + +#ifndef CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_ +#define CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_ + +#include <memory> + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/backoff_entry.h" +#include "services/network/public/cpp/network_connection_tracker.h" + +class Profile; + +namespace base { +class FilePath; +} + +namespace signin { +class IdentityManager; +class PrimaryAccountAccessTokenFetcher; +struct AccessTokenInfo; +} // namespace signin + +// ForceSigninVerifier will verify profile's auth token when profile is loaded +// into memory by the first time via gaia server. It will retry on any transient +// error. +class ForceSigninVerifier + : public network::NetworkConnectionTracker::NetworkConnectionObserver { + public: + explicit ForceSigninVerifier(Profile* profile, + signin::IdentityManager* identity_manager); + + ForceSigninVerifier(const ForceSigninVerifier&) = delete; + ForceSigninVerifier& operator=(const ForceSigninVerifier&) = delete; + + ~ForceSigninVerifier() override; + + void OnAccessTokenFetchComplete(GoogleServiceAuthError error, + signin::AccessTokenInfo token_info); + + // override network::NetworkConnectionTracker::NetworkConnectionObserver + void OnConnectionChanged(network::mojom::ConnectionType type) override; + + // Cancel any pending or ongoing verification. + void Cancel(); + + // Return the value of |has_token_verified_|. + bool HasTokenBeenVerified(); + + protected: + // Send the token verification request. The request will be sent only if + // - The token has never been verified before. + // - There is no on going verification. + // - There is network connection. + // - The profile has signed in. + void SendRequest(); + + // Send the request if |network_type| is not CONNECTION_NONE and + // ShouldSendRequest returns true. + void SendRequestIfNetworkAvailable( + network::mojom::ConnectionType network_type); + + bool ShouldSendRequest(); + + virtual void CloseAllBrowserWindows(); + void OnCloseBrowsersSuccess(const base::FilePath& profile_path); + + signin::PrimaryAccountAccessTokenFetcher* GetAccessTokenFetcherForTesting(); + net::BackoffEntry* GetBackoffEntryForTesting(); + base::OneShotTimer* GetOneShotTimerForTesting(); + + private: + std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher> + access_token_fetcher_; + + // Indicates whether the verification is finished successfully or with a + // persistent error. + bool has_token_verified_ = false; + net::BackoffEntry backoff_entry_; + base::OneShotTimer backoff_request_timer_; + base::TimeTicks creation_time_; + + raw_ptr<Profile> profile_ = nullptr; + raw_ptr<signin::IdentityManager> identity_manager_ = nullptr; + + base::WeakPtrFactory<ForceSigninVerifier> weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_FORCE_SIGNIN_VERIFIER_H_ diff --git a/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc b/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc new file mode 100644 index 00000000000..dcd382d6b38 --- /dev/null +++ b/chromium/chrome/browser/signin/force_signin_verifier_unittest.cc @@ -0,0 +1,416 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/force_signin_verifier.h" + +#include "base/run_loop.h" +#include "base/test/task_environment.h" +#include "base/threading/thread_task_runner_handle.h" +#include "chrome/browser/profiles/profile.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "content/public/browser/network_service_instance.h" +#include "services/network/test/test_network_connection_tracker.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class ForceSigninVerifierWithAccessToInternalsForTesting + : public ForceSigninVerifier { + public: + explicit ForceSigninVerifierWithAccessToInternalsForTesting( + signin::IdentityManager* identity_manager) + : ForceSigninVerifier(nullptr, identity_manager) {} + + bool IsDelayTaskPosted() { return GetOneShotTimerForTesting()->IsRunning(); } + + int FailureCount() { return GetBackoffEntryForTesting()->failure_count(); } + + signin::PrimaryAccountAccessTokenFetcher* access_token_fetcher() { + return GetAccessTokenFetcherForTesting(); + } + + MOCK_METHOD0(CloseAllBrowserWindows, void(void)); +}; + +// A NetworkConnectionObserver that invokes a base::RepeatingClosure when +// NetworkConnectionObserver::OnConnectionChanged() is invoked. +class NetworkConnectionObserverHelper + : public network::NetworkConnectionTracker::NetworkConnectionObserver { + public: + explicit NetworkConnectionObserverHelper(base::RepeatingClosure closure) + : closure_(std::move(closure)) { + DCHECK(!closure_.is_null()); + content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this); + } + + NetworkConnectionObserverHelper(const NetworkConnectionObserverHelper&) = + delete; + NetworkConnectionObserverHelper& operator=( + const NetworkConnectionObserverHelper&) = delete; + + ~NetworkConnectionObserverHelper() override { + content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver( + this); + } + + void OnConnectionChanged(network::mojom::ConnectionType type) override { + closure_.Run(); + } + + private: + base::RepeatingClosure closure_; +}; + +// Used to select which type of network type NetworkConnectionTracker should +// be configured to. +enum class NetworkConnectionType { + Undecided, + ConnectionNone, + ConnectionWifi, + Connection4G, +}; + +// Used to select which type of response NetworkConnectionTracker should give. +enum class NetworkResponseType { + Undecided, + Synchronous, + Asynchronous, +}; + +// Forces the network connection type to change to |connection_type| and wait +// till the notification has been propagated to the observers. Also change the +// response type to be synchronous/asynchronous based on |response_type|. +void ConfigureNetworkConnectionTracker(NetworkConnectionType connection_type, + NetworkResponseType response_type) { + network::TestNetworkConnectionTracker* tracker = + network::TestNetworkConnectionTracker::GetInstance(); + + switch (response_type) { + case NetworkResponseType::Undecided: + // nothing to do + break; + + case NetworkResponseType::Synchronous: + tracker->SetRespondSynchronously(true); + break; + + case NetworkResponseType::Asynchronous: + tracker->SetRespondSynchronously(false); + break; + } + + if (connection_type != NetworkConnectionType::Undecided) { + network::mojom::ConnectionType mojom_connection_type = + network::mojom::ConnectionType::CONNECTION_UNKNOWN; + + switch (connection_type) { + case NetworkConnectionType::Undecided: + NOTREACHED(); + break; + + case NetworkConnectionType::ConnectionNone: + mojom_connection_type = network::mojom::ConnectionType::CONNECTION_NONE; + break; + + case NetworkConnectionType::ConnectionWifi: + mojom_connection_type = network::mojom::ConnectionType::CONNECTION_WIFI; + break; + + case NetworkConnectionType::Connection4G: + mojom_connection_type = network::mojom::ConnectionType::CONNECTION_4G; + break; + } + + DCHECK_NE(mojom_connection_type, + network::mojom::ConnectionType::CONNECTION_UNKNOWN); + + base::RunLoop wait_for_network_type_change; + NetworkConnectionObserverHelper scoped_observer( + wait_for_network_type_change.QuitWhenIdleClosure()); + + tracker->SetConnectionType(mojom_connection_type); + + wait_for_network_type_change.Run(); + } +} + +// Forces the current sequence's task runner to spin. This is used because the +// ForceSigninVerifier ends up posting task to the sequence's task runner when +// MetworkConnectionTracker is returning results asynchronously. +void SpinCurrentSequenceTaskRunner() { + base::RunLoop run_loop; + base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, + run_loop.QuitClosure()); + run_loop.Run(); +} + +} // namespace + +TEST(ForceSigninVerifierTest, OnGetTokenSuccess) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + ASSERT_NE(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(0); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( + account_info.account_id, /*token=*/"", base::Time()); + + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_TRUE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + ASSERT_EQ(0, verifier.FailureCount()); +} + +TEST(ForceSigninVerifierTest, OnGetTokenPersistentFailure) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + ASSERT_NE(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(1); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + GoogleServiceAuthError( + GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS)); + + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_TRUE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + ASSERT_EQ(0, verifier.FailureCount()); +} + +TEST(ForceSigninVerifierTest, OnGetTokenTransientFailure) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + ASSERT_NE(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.HasTokenBeenVerified()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); + EXPECT_CALL(verifier, CloseAllBrowserWindows()).Times(0); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED)); + + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.HasTokenBeenVerified()); + ASSERT_TRUE(verifier.IsDelayTaskPosted()); + ASSERT_EQ(1, verifier.FailureCount()); +} + +TEST(ForceSigninVerifierTest, OnLostConnection) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED)); + + ASSERT_EQ(1, verifier.FailureCount()); + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_TRUE(verifier.IsDelayTaskPosted()); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionNone, + NetworkResponseType::Undecided); + + ASSERT_EQ(0, verifier.FailureCount()); + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); +} + +TEST(ForceSigninVerifierTest, OnReconnected) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithError( + GoogleServiceAuthError(GoogleServiceAuthError::State::CONNECTION_FAILED)); + + ASSERT_EQ(1, verifier.FailureCount()); + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + ASSERT_TRUE(verifier.IsDelayTaskPosted()); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi, + NetworkResponseType::Undecided); + + ASSERT_EQ(0, verifier.FailureCount()); + ASSERT_NE(nullptr, verifier.access_token_fetcher()); + ASSERT_FALSE(verifier.IsDelayTaskPosted()); +} + +TEST(ForceSigninVerifierTest, GetNetworkStatusAsync) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::Undecided, + NetworkResponseType::Asynchronous); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + // There is no network type at first. + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + + // Waiting for the network type returns. + SpinCurrentSequenceTaskRunner(); + + // Get the type and send the request. + ASSERT_NE(nullptr, verifier.access_token_fetcher()); +} + +TEST(ForceSigninVerifierTest, LaunchVerifierWithoutNetwork) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionNone, + NetworkResponseType::Asynchronous); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + // There is no network type. + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + + // Waiting for the network type returns. + SpinCurrentSequenceTaskRunner(); + + // Get the type, there is no network connection, don't send the request. + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + + // Network is resumed. + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi, + NetworkResponseType::Undecided); + + // Send the request. + ASSERT_NE(nullptr, verifier.access_token_fetcher()); +} + +TEST(ForceSigninVerifierTest, ChangeNetworkFromWIFITo4GWithOnGoingRequest) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi, + NetworkResponseType::Asynchronous); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + EXPECT_EQ(nullptr, verifier.access_token_fetcher()); + + // Waiting for the network type returns. + SpinCurrentSequenceTaskRunner(); + + // The network type if wifi, send the request. + auto* first_request = verifier.access_token_fetcher(); + EXPECT_NE(nullptr, first_request); + + // Network is changed to 4G. + ConfigureNetworkConnectionTracker(NetworkConnectionType::Connection4G, + NetworkResponseType::Undecided); + + // There is still one on-going request. + EXPECT_EQ(first_request, verifier.access_token_fetcher()); + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( + account_info.account_id, /*token=*/"", base::Time()); +} + +TEST(ForceSigninVerifierTest, ChangeNetworkFromWIFITo4GWithFinishedRequest) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::ConnectionWifi, + NetworkResponseType::Asynchronous); + + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + EXPECT_EQ(nullptr, verifier.access_token_fetcher()); + + // Waiting for the network type returns. + SpinCurrentSequenceTaskRunner(); + + // The network type if wifi, send the request. + EXPECT_NE(nullptr, verifier.access_token_fetcher()); + + // Finishes the request. + identity_test_env.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken( + account_info.account_id, /*token=*/"", base::Time()); + EXPECT_EQ(nullptr, verifier.access_token_fetcher()); + + // Network is changed to 4G. + ConfigureNetworkConnectionTracker(NetworkConnectionType::Connection4G, + NetworkResponseType::Undecided); + + // No more request because it's verfied already. + EXPECT_EQ(nullptr, verifier.access_token_fetcher()); +} + +// Regression test for https://crbug.com/1259864 +TEST(ForceSigninVerifierTest, DeleteWithPendingRequestShouldNotCrash) { + base::test::TaskEnvironment scoped_task_env; + signin::IdentityTestEnvironment identity_test_env; + const AccountInfo account_info = + identity_test_env.MakePrimaryAccountAvailable( + "email@test.com", signin::ConsentLevel::kSync); + + ConfigureNetworkConnectionTracker(NetworkConnectionType::Undecided, + NetworkResponseType::Asynchronous); + + { + ForceSigninVerifierWithAccessToInternalsForTesting verifier( + identity_test_env.identity_manager()); + + // There is no network type at first. + ASSERT_EQ(nullptr, verifier.access_token_fetcher()); + + // Delete the verifier while the request is pending. + } + + // Waiting for the network type returns, this should not crash. + SpinCurrentSequenceTaskRunner(); +} diff --git a/chromium/chrome/browser/signin/header_modification_delegate.h b/chromium/chrome/browser/signin/header_modification_delegate.h new file mode 100644 index 00000000000..8d3bce133ac --- /dev/null +++ b/chromium/chrome/browser/signin/header_modification_delegate.h @@ -0,0 +1,38 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_ +#define CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_ + +class GURL; + +namespace content { +class WebContents; +} + +namespace signin { + +class ChromeRequestAdapter; +class ResponseAdapter; + +class HeaderModificationDelegate { + public: + HeaderModificationDelegate() = default; + + HeaderModificationDelegate(const HeaderModificationDelegate&) = delete; + HeaderModificationDelegate& operator=(const HeaderModificationDelegate&) = + delete; + + virtual ~HeaderModificationDelegate() = default; + + virtual bool ShouldInterceptNavigation(content::WebContents* contents) = 0; + virtual void ProcessRequest(ChromeRequestAdapter* request_adapter, + const GURL& redirect_url) = 0; + virtual void ProcessResponse(ResponseAdapter* response_adapter, + const GURL& redirect_url) = 0; +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_H_ diff --git a/chromium/chrome/browser/signin/header_modification_delegate_impl.cc b/chromium/chrome/browser/signin/header_modification_delegate_impl.cc new file mode 100644 index 00000000000..6aa2d10614f --- /dev/null +++ b/chromium/chrome/browser/signin/header_modification_delegate_impl.cc @@ -0,0 +1,154 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/header_modification_delegate_impl.h" + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/content_settings/cookie_settings_factory.h" +#include "chrome/browser/extensions/api/identity/web_auth_flow.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/chrome_signin_helper.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/sync/sync_service_factory.h" +#include "chrome/common/pref_names.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/tribool.h" +#include "components/sync/base/pref_names.h" +#include "components/sync/driver/sync_service.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/site_instance.h" + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "extensions/browser/guest_view/web_view/web_view_guest.h" +#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "components/account_manager_core/pref_names.h" +#endif + +namespace signin { + +#if defined(OS_ANDROID) +HeaderModificationDelegateImpl::HeaderModificationDelegateImpl( + Profile* profile, + bool incognito_enabled) + : profile_(profile), + cookie_settings_(CookieSettingsFactory::GetForProfile(profile_)), + incognito_enabled_(incognito_enabled) {} +#else +HeaderModificationDelegateImpl::HeaderModificationDelegateImpl(Profile* profile) + : profile_(profile), + cookie_settings_(CookieSettingsFactory::GetForProfile(profile_)) {} +#endif + +HeaderModificationDelegateImpl::~HeaderModificationDelegateImpl() = default; + +bool HeaderModificationDelegateImpl::ShouldInterceptNavigation( + content::WebContents* contents) { + if (profile_->IsOffTheRecord()) + return false; + +#if BUILDFLAG(ENABLE_EXTENSIONS) + if (ShouldIgnoreGuestWebViewRequest(contents)) + return false; +#endif + + return true; +} + +void HeaderModificationDelegateImpl::ProcessRequest( + ChromeRequestAdapter* request_adapter, + const GURL& redirect_url) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + const PrefService* prefs = profile_->GetPrefs(); +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + syncer::SyncService* sync_service = + SyncServiceFactory::GetForProfile(profile_); +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) + bool is_secondary_account_addition_allowed = true; + if (!prefs->GetBoolean( + ::account_manager::prefs::kSecondaryGoogleAccountSigninAllowed)) { + is_secondary_account_addition_allowed = false; + } +#endif + + ConsentLevel consent_level = ConsentLevel::kSync; +#if defined(OS_ANDROID) + consent_level = ConsentLevel::kSignin; +#endif + + IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile_); + CoreAccountInfo account = + identity_manager->GetPrimaryAccountInfo(consent_level); + signin::Tribool is_child_account = + // Defaults to kUnknown if the account is not found. + identity_manager->FindExtendedAccountInfo(account).is_child_account; + + int incognito_mode_availability = + prefs->GetInteger(prefs::kIncognitoModeAvailability); +#if defined(OS_ANDROID) + incognito_mode_availability = + incognito_enabled_ + ? incognito_mode_availability + : static_cast<int>(IncognitoModePrefs::Availability::kDisabled); +#endif + + FixAccountConsistencyRequestHeader( + request_adapter, redirect_url, profile_->IsOffTheRecord(), + incognito_mode_availability, + AccountConsistencyModeManager::GetMethodForProfile(profile_), + account.gaia, is_child_account, +#if BUILDFLAG(IS_CHROMEOS_ASH) + is_secondary_account_addition_allowed, +#endif +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + sync_service && sync_service->IsSyncFeatureEnabled(), + prefs->GetString(prefs::kGoogleServicesSigninScopedDeviceId), +#endif + cookie_settings_.get()); +} + +void HeaderModificationDelegateImpl::ProcessResponse( + ResponseAdapter* response_adapter, + const GURL& redirect_url) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + ProcessAccountConsistencyResponseHeaders(response_adapter, redirect_url, + profile_->IsOffTheRecord()); +} + +#if BUILDFLAG(ENABLE_EXTENSIONS) +// static +bool HeaderModificationDelegateImpl::ShouldIgnoreGuestWebViewRequest( + content::WebContents* contents) { + if (!contents) + return true; + + if (extensions::WebViewRendererState::GetInstance()->IsGuest( + contents->GetMainFrame()->GetProcess()->GetID())) { + auto identity_api_config = + extensions::WebAuthFlow::GetWebViewPartitionConfig( + extensions::WebAuthFlow::GET_AUTH_TOKEN, + contents->GetBrowserContext()); + if (contents->GetSiteInstance()->GetStoragePartitionConfig() != + identity_api_config) + return true; + + // If the StoragePartitionConfig matches, but |contents| is not using a + // guest SiteInstance, then there is likely a serious bug. + CHECK(contents->GetSiteInstance()->IsGuest()); + } + return false; +} +#endif + +} // namespace signin diff --git a/chromium/chrome/browser/signin/header_modification_delegate_impl.h b/chromium/chrome/browser/signin/header_modification_delegate_impl.h new file mode 100644 index 00000000000..8bc1fd7fb2d --- /dev/null +++ b/chromium/chrome/browser/signin/header_modification_delegate_impl.h @@ -0,0 +1,68 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_ +#define CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_ + +#include "base/memory/raw_ptr.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "chrome/browser/signin/header_modification_delegate.h" +#include "components/content_settings/core/browser/cookie_settings.h" +#include "content/public/browser/browser_thread.h" +#include "extensions/buildflags/buildflags.h" + +class Profile; + +namespace signin { + +// This class wraps the FixAccountConsistencyRequestHeader and +// ProcessAccountConsistencyResponseHeaders in the HeaderModificationDelegate +// interface. +class HeaderModificationDelegateImpl : public HeaderModificationDelegate { + public: +#if defined(OS_ANDROID) + explicit HeaderModificationDelegateImpl(Profile* profile, + bool incognito_enabled); +#else + explicit HeaderModificationDelegateImpl(Profile* profile); +#endif + + HeaderModificationDelegateImpl(const HeaderModificationDelegateImpl&) = + delete; + HeaderModificationDelegateImpl& operator=( + const HeaderModificationDelegateImpl&) = delete; + + ~HeaderModificationDelegateImpl() override; + + // HeaderModificationDelegate + bool ShouldInterceptNavigation(content::WebContents* contents) override; + void ProcessRequest(ChromeRequestAdapter* request_adapter, + const GURL& redirect_url) override; + void ProcessResponse(ResponseAdapter* response_adapter, + const GURL& redirect_url) override; + +#if BUILDFLAG(ENABLE_EXTENSIONS) + // Returns true if the request comes from a web view and should be ignored + // (i.e. not intercepted). + // Returns false if the request does not come from a web view. + // Requests coming from most guest web views are ignored. In particular the + // requests coming from the InlineLoginUI are not intercepted (see + // http://crbug.com/428396). Requests coming from the chrome identity + // extension consent flow are not ignored. + static bool ShouldIgnoreGuestWebViewRequest(content::WebContents* contents); +#endif + + private: + raw_ptr<Profile> profile_; + scoped_refptr<content_settings::CookieSettings> cookie_settings_; + +#if defined(OS_ANDROID) + bool incognito_enabled_; +#endif +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_HEADER_MODIFICATION_DELEGATE_IMPL_H_ diff --git a/chromium/chrome/browser/signin/identity_manager_factory.cc b/chromium/chrome/browser/signin/identity_manager_factory.cc new file mode 100644 index 00000000000..cac771bf1e2 --- /dev/null +++ b/chromium/chrome/browser/signin/identity_manager_factory.cc @@ -0,0 +1,171 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/identity_manager_factory.h" + +#include <memory> +#include <utility> + +#include "base/files/file_path.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/image_fetcher/image_decoder_impl.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_provider.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_manager_builder.h" +#include "components/signin/public/webdata/token_web_data.h" +#include "content/public/browser/network_service_instance.h" + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "chrome/browser/content_settings/cookie_settings_factory.h" +#include "chrome/browser/web_data_service_factory.h" +#include "components/content_settings/core/browser/cookie_settings.h" +#include "components/keyed_service/core/service_access_type.h" +#include "components/signin/core/browser/cookie_settings_util.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/profiles/profile_helper.h" +#include "chrome/browser/browser_process_platform_part.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "chrome/browser/lacros/account_manager/profile_account_manager.h" +#include "chrome/browser/lacros/account_manager/profile_account_manager_factory.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif + +#if defined(OS_WIN) +#include "base/bind.h" +#include "chrome/browser/signin/signin_util_win.h" +#endif + +void IdentityManagerFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + signin::IdentityManager::RegisterProfilePrefs(registry); +} + +IdentityManagerFactory::IdentityManagerFactory() + : BrowserContextKeyedServiceFactory( + "IdentityManager", + BrowserContextDependencyManager::GetInstance()) { +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + DependsOn(WebDataServiceFactory::GetInstance()); +#endif +#if BUILDFLAG(IS_CHROMEOS_LACROS) + DependsOn(ProfileAccountManagerFactory::GetInstance()); +#endif + DependsOn(ChromeSigninClientFactory::GetInstance()); + signin::SetIdentityManagerProvider( + base::BindRepeating([](content::BrowserContext* context) { + return GetForProfile(Profile::FromBrowserContext(context)); + })); +} + +IdentityManagerFactory::~IdentityManagerFactory() { + signin::SetIdentityManagerProvider({}); +} + +// static +signin::IdentityManager* IdentityManagerFactory::GetForProfile( + Profile* profile) { + return static_cast<signin::IdentityManager*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +signin::IdentityManager* IdentityManagerFactory::GetForProfileIfExists( + const Profile* profile) { + return static_cast<signin::IdentityManager*>( + GetInstance()->GetServiceForBrowserContext(const_cast<Profile*>(profile), + false)); +} + +// static +IdentityManagerFactory* IdentityManagerFactory::GetInstance() { + return base::Singleton<IdentityManagerFactory>::get(); +} + +// static +void IdentityManagerFactory::EnsureFactoryAndDependeeFactoriesBuilt() { + IdentityManagerFactory::GetInstance(); + ChromeSigninClientFactory::GetInstance(); +} + +void IdentityManagerFactory::AddObserver(Observer* observer) { + observer_list_.AddObserver(observer); +} + +void IdentityManagerFactory::RemoveObserver(Observer* observer) { + observer_list_.RemoveObserver(observer); +} + +KeyedService* IdentityManagerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + + signin::IdentityManagerBuildParams params; + params.account_consistency = + AccountConsistencyModeManager::GetMethodForProfile(profile), + params.image_decoder = std::make_unique<ImageDecoderImpl>(); + params.local_state = g_browser_process->local_state(); + params.network_connection_tracker = content::GetNetworkConnectionTracker(); + params.pref_service = profile->GetPrefs(); + params.profile_path = profile->GetPath(); + params.signin_client = ChromeSigninClientFactory::GetForProfile(profile); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + params.delete_signin_cookies_on_exit = + signin::SettingsDeleteSigninCookiesOnExit( + CookieSettingsFactory::GetForProfile(profile).get()); + params.token_web_data = WebDataServiceFactory::GetTokenWebDataForProfile( + profile, ServiceAccessType::EXPLICIT_ACCESS); +#endif + +#if BUILDFLAG(IS_CHROMEOS_ASH) + params.account_manager_facade = + GetAccountManagerFacade(profile->GetPath().value()); + params.is_regular_profile = + chromeos::ProfileHelper::IsRegularProfile(profile); +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) + // The system and (original profile of the) guest profiles are not regular. + const bool is_regular_profile = profile->IsRegularProfile(); + const bool use_profile_account_manager = + is_regular_profile && + // `ProfileManager` may be null in tests, and is required for account + // consistency. + g_browser_process->profile_manager(); + + params.account_manager_facade = + use_profile_account_manager + ? ProfileAccountManagerFactory::GetForProfile(profile) + : GetAccountManagerFacade(profile->GetPath().value()); + params.is_regular_profile = is_regular_profile; +#endif + +#if defined(OS_WIN) + params.reauth_callback = + base::BindRepeating(&signin_util::ReauthWithCredentialProviderIfPossible, + base::Unretained(profile)); +#endif + + std::unique_ptr<signin::IdentityManager> identity_manager = + signin::BuildIdentityManager(¶ms); + + for (Observer& observer : observer_list_) + observer.IdentityManagerCreated(identity_manager.get()); + + return identity_manager.release(); +} diff --git a/chromium/chrome/browser/signin/identity_manager_factory.h b/chromium/chrome/browser/signin/identity_manager_factory.h new file mode 100644 index 00000000000..a2e0590da42 --- /dev/null +++ b/chromium/chrome/browser/signin/identity_manager_factory.h @@ -0,0 +1,67 @@ +// Copyright 2017 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. + +#ifndef CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "base/observer_list.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace signin { +class IdentityManager; +} + +class Profile; + +// Singleton that owns all IdentityManager instances and associates them with +// Profiles. +class IdentityManagerFactory : public BrowserContextKeyedServiceFactory { + public: + class Observer : public base::CheckedObserver { + public: + // Called when a IdentityManager instance is created. + virtual void IdentityManagerCreated( + signin::IdentityManager* identity_manager) {} + + protected: + ~Observer() override {} + }; + + static signin::IdentityManager* GetForProfile(Profile* profile); + static signin::IdentityManager* GetForProfileIfExists(const Profile* profile); + + // Returns an instance of the IdentityManagerFactory singleton. + static IdentityManagerFactory* GetInstance(); + + IdentityManagerFactory(const IdentityManagerFactory&) = delete; + IdentityManagerFactory& operator=(const IdentityManagerFactory&) = delete; + + // Ensures that IdentityManagerFactory and the factories on which it depends + // are built. + static void EnsureFactoryAndDependeeFactoriesBuilt(); + + // Methods to register or remove observers of IdentityManager + // creation/shutdown. + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + private: + friend struct base::DefaultSingletonTraits<IdentityManagerFactory>; + + IdentityManagerFactory(); + ~IdentityManagerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) override; + + // List of observers. Checks that list is empty on destruction. + base::ObserverList<Observer, /*check_empty=*/true, /*allow_reentrancy=*/false> + observer_list_; +}; + +#endif // CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/identity_manager_provider.cc b/chromium/chrome/browser/signin/identity_manager_provider.cc new file mode 100644 index 00000000000..ca42e587adf --- /dev/null +++ b/chromium/chrome/browser/signin/identity_manager_provider.cc @@ -0,0 +1,38 @@ +// Copyright 2021 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. + +#include "chrome/browser/signin/identity_manager_provider.h" + +#include "base/check.h" +#include "base/no_destructor.h" + +namespace signin { + +namespace { + +IdentityManagerProvider& GetIdentityManagerProvider() { + static base::NoDestructor<IdentityManagerProvider> provider; + return *provider; +} + +} // namespace + +void SetIdentityManagerProvider(const IdentityManagerProvider& provider) { + IdentityManagerProvider& instance = GetIdentityManagerProvider(); + + // Exactly one of `provider` or `instance` should be non-null. + if (provider) + DCHECK(!instance); + else + DCHECK(instance); + + instance = provider; +} + +IdentityManager* GetIdentityManagerForBrowserContext( + content::BrowserContext* context) { + return GetIdentityManagerProvider().Run(context); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/identity_manager_provider.h b/chromium/chrome/browser/signin/identity_manager_provider.h new file mode 100644 index 00000000000..9b6291d643b --- /dev/null +++ b/chromium/chrome/browser/signin/identity_manager_provider.h @@ -0,0 +1,32 @@ +// Copyright 2021 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. + +#ifndef CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_ +#define CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_ + +#include "base/callback.h" + +namespace content { +class BrowserContext; +} + +namespace signin { + +class IdentityManager; + +using IdentityManagerProvider = + base::RepeatingCallback<IdentityManager*(content::BrowserContext*)>; + +// Called by IdentityManagerFactory to expose a way to retrieve the +// IdentityManager for a specific BrowserContext/Profile. This exists so that +// components which don't depend on //chrome/browser can still access the +// IdentityManager. +void SetIdentityManagerProvider(const IdentityManagerProvider& provider); + +IdentityManager* GetIdentityManagerForBrowserContext( + content::BrowserContext* context); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_IDENTITY_MANAGER_PROVIDER_H_ diff --git a/chromium/chrome/browser/signin/identity_services_provider_android.cc b/chromium/chrome/browser/signin/identity_services_provider_android.cc new file mode 100644 index 00000000000..cb49ede8c7d --- /dev/null +++ b/chromium/chrome/browser/signin/identity_services_provider_android.cc @@ -0,0 +1,39 @@ +// Copyright 2018 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. + +#include "base/android/jni_android.h" +#include "chrome/browser/profiles/profile_android.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/services/android/jni_headers/IdentityServicesProvider_jni.h" +#include "chrome/browser/signin/signin_manager_android_factory.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +using base::android::JavaParamRef; +using base::android::ScopedJavaLocalRef; + +static ScopedJavaLocalRef<jobject> +JNI_IdentityServicesProvider_GetIdentityManager( + JNIEnv* env, + const JavaParamRef<jobject>& j_profile_android) { + Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android); + return IdentityManagerFactory::GetForProfile(profile)->GetJavaObject(); +} + +static ScopedJavaLocalRef<jobject> +JNI_IdentityServicesProvider_GetAccountTrackerService( + JNIEnv* env, + const JavaParamRef<jobject>& j_profile_android) { + Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + return identity_manager->LegacyGetAccountTrackerServiceJavaObject(); +} + +static ScopedJavaLocalRef<jobject> +JNI_IdentityServicesProvider_GetSigninManager( + JNIEnv* env, + const JavaParamRef<jobject>& j_profile_android) { + Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android); + return SigninManagerAndroidFactory::GetJavaObjectForProfile(profile); +} diff --git a/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc new file mode 100644 index 00000000000..ff94a971878 --- /dev/null +++ b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.cc @@ -0,0 +1,103 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" + +#include "base/bind.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/signin/chrome_signin_client_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "ash/components/account_manager/account_manager_factory.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/browser_process_platform_part.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif + +// static +std::unique_ptr<TestingProfile> IdentityTestEnvironmentProfileAdaptor:: + CreateProfileForIdentityTestEnvironment() { + return CreateProfileForIdentityTestEnvironment( + TestingProfile::TestingFactories()); +} + +// static +std::unique_ptr<TestingProfile> +IdentityTestEnvironmentProfileAdaptor::CreateProfileForIdentityTestEnvironment( + const TestingProfile::TestingFactories& input_factories) { + TestingProfile::Builder builder; + + for (auto& input_factory : input_factories) { + builder.AddTestingFactory(input_factory.first, input_factory.second); + } + + return CreateProfileForIdentityTestEnvironment(builder); +} + +// static +std::unique_ptr<TestingProfile> +IdentityTestEnvironmentProfileAdaptor::CreateProfileForIdentityTestEnvironment( + TestingProfile::Builder& builder, + signin::AccountConsistencyMethod account_consistency) { + for (auto& identity_factory : + GetIdentityTestEnvironmentFactories(account_consistency)) { + builder.AddTestingFactory(identity_factory.first, identity_factory.second); + } + + return builder.Build(); +} + +// static +void IdentityTestEnvironmentProfileAdaptor:: + SetIdentityTestEnvironmentFactoriesOnBrowserContext( + content::BrowserContext* context) { + for (const auto& factory_pair : GetIdentityTestEnvironmentFactories()) { + factory_pair.first->SetTestingFactory(context, factory_pair.second); + } +} + +// static +void IdentityTestEnvironmentProfileAdaptor:: + AppendIdentityTestEnvironmentFactories( + TestingProfile::TestingFactories* factories_to_append_to) { + TestingProfile::TestingFactories identity_factories = + GetIdentityTestEnvironmentFactories(); + factories_to_append_to->insert(factories_to_append_to->end(), + identity_factories.begin(), + identity_factories.end()); +} + +// static +TestingProfile::TestingFactories +IdentityTestEnvironmentProfileAdaptor::GetIdentityTestEnvironmentFactories( + signin::AccountConsistencyMethod account_consistency) { + return {{IdentityManagerFactory::GetInstance(), + base::BindRepeating(&BuildIdentityManagerForTests, + account_consistency)}}; +} + +// static +std::unique_ptr<KeyedService> +IdentityTestEnvironmentProfileAdaptor::BuildIdentityManagerForTests( + signin::AccountConsistencyMethod account_consistency, + content::BrowserContext* context) { + Profile* profile = Profile::FromBrowserContext(context); +#if BUILDFLAG(IS_CHROMEOS_ASH) + return signin::IdentityTestEnvironment::BuildIdentityManagerForTests( + ChromeSigninClientFactory::GetForProfile(profile), profile->GetPrefs(), + profile->GetPath(), + g_browser_process->platform_part()->GetAccountManagerFactory(), + GetAccountManagerFacade(profile->GetPath().value())); +#else + return signin::IdentityTestEnvironment::BuildIdentityManagerForTests( + ChromeSigninClientFactory::GetForProfile(profile), profile->GetPrefs(), + profile->GetPath(), account_consistency); +#endif +} + +IdentityTestEnvironmentProfileAdaptor::IdentityTestEnvironmentProfileAdaptor( + Profile* profile) + : identity_test_env_(IdentityManagerFactory::GetForProfile(profile), + ChromeSigninClientFactory::GetForProfile(profile)) {} diff --git a/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h new file mode 100644 index 00000000000..ef477efd9d3 --- /dev/null +++ b/chromium/chrome/browser/signin/identity_test_environment_profile_adaptor.h @@ -0,0 +1,101 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_ +#define CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_ + +#include "chrome/test/base/testing_profile.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" + +// Adaptor that supports signin::IdentityTestEnvironment's usage in testing +// contexts where the relevant fake objects must be injected via the +// BrowserContextKeyedServiceFactory infrastructure as the production code +// accesses IdentityManager via that infrastructure. Before using this +// class, please consider whether you can change the production code in question +// to take in the relevant dependencies directly rather than obtaining them from +// the Profile; this is both cleaner in general and allows for direct usage of +// signin::IdentityTestEnvironment in the test. +class IdentityTestEnvironmentProfileAdaptor { + public: + // Creates and returns a TestingProfile that has been configured with the set + // of testing factories that IdentityTestEnvironment requires. + static std::unique_ptr<TestingProfile> + CreateProfileForIdentityTestEnvironment(); + + // Like the above, but additionally configures the returned Profile with + // |input_factories|. + static std::unique_ptr<TestingProfile> + CreateProfileForIdentityTestEnvironment( + const TestingProfile::TestingFactories& input_factories); + + // Creates and returns a TestingProfile that has been configured with the + // given |builder| and the set of testing factories that + // IdentityTestEnvironment requires. + // See the above variant for comments on common parameters. + static std::unique_ptr<TestingProfile> + CreateProfileForIdentityTestEnvironment( + TestingProfile::Builder& builder, + signin::AccountConsistencyMethod account_consistency = + signin::AccountConsistencyMethod::kDisabled); + + // Sets the testing factories that signin::IdentityTestEnvironment + // requires explicitly on a Profile that is passed to it. + // See the above variant for comments on common parameters. + static void SetIdentityTestEnvironmentFactoriesOnBrowserContext( + content::BrowserContext* browser_context); + + // Appends the set of testing factories that signin::IdentityTestEnvironment + // requires to |factories_to_append_to|, which should be the set of testing + // factories supplied to TestingProfile (via one of the various mechanisms for + // doing so). Prefer the above API if possible, as it is less fragile. This + // API is primarily for use in tests that do not create the TestingProfile + // internally but rather simply supply the set of TestingFactories to some + // external facility (e.g., a superclass). + // See CreateProfileForIdentityTestEnvironment() for comments on common + // parameters. + static void AppendIdentityTestEnvironmentFactories( + TestingProfile::TestingFactories* factories_to_append_to); + + // Returns the set of testing factories that signin::IdentityTestEnvironment + // requires, which can be useful to configure profiles for services that do + // not require any other testing factory than the ones specified in here. + static TestingProfile::TestingFactories GetIdentityTestEnvironmentFactories( + signin::AccountConsistencyMethod account_consistency = + signin::AccountConsistencyMethod::kDisabled); + + // Constructs an adaptor that associates an IdentityTestEnvironment instance + // with |profile| via the relevant backing objects. Note that + // |profile| must have been configured with the IdentityTestEnvironment + // testing factories, either because it was created via + // CreateProfileForIdentityTestEnvironment() or because + // AppendIdentityTestEnvironmentFactories() was invoked on the set of + // factories supplied to it. + // |profile| must outlive this object. + explicit IdentityTestEnvironmentProfileAdaptor(Profile* profile); + + IdentityTestEnvironmentProfileAdaptor( + const IdentityTestEnvironmentProfileAdaptor&) = delete; + IdentityTestEnvironmentProfileAdaptor& operator=( + const IdentityTestEnvironmentProfileAdaptor&) = delete; + + ~IdentityTestEnvironmentProfileAdaptor() {} + + // Returns the IdentityTestEnvironment associated with this object (and + // implicitly with the Profile passed to this object's constructor). + signin::IdentityTestEnvironment* identity_test_env() { + return &identity_test_env_; + } + + private: + // Testing factory that creates an IdentityManager + // with a FakeProfileOAuth2TokenService. + static std::unique_ptr<KeyedService> BuildIdentityManagerForTests( + signin::AccountConsistencyMethod account_consistency, + content::BrowserContext* context); + + signin::IdentityTestEnvironment identity_test_env_; +}; + +#endif // CHROME_BROWSER_SIGNIN_IDENTITY_TEST_ENVIRONMENT_PROFILE_ADAPTOR_H_ diff --git a/chromium/chrome/browser/signin/investigator_dependency_provider.cc b/chromium/chrome/browser/signin/investigator_dependency_provider.cc new file mode 100644 index 00000000000..601806d2669 --- /dev/null +++ b/chromium/chrome/browser/signin/investigator_dependency_provider.cc @@ -0,0 +1,14 @@ +// 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. + +#include "chrome/browser/signin/investigator_dependency_provider.h" + +InvestigatorDependencyProvider::InvestigatorDependencyProvider(Profile* profile) + : profile_(profile) {} + +InvestigatorDependencyProvider::~InvestigatorDependencyProvider() {} + +PrefService* InvestigatorDependencyProvider::GetPrefs() { + return profile_->GetPrefs(); +} diff --git a/chromium/chrome/browser/signin/investigator_dependency_provider.h b/chromium/chrome/browser/signin/investigator_dependency_provider.h new file mode 100644 index 00000000000..199f7bb07a5 --- /dev/null +++ b/chromium/chrome/browser/signin/investigator_dependency_provider.h @@ -0,0 +1,33 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_ +#define CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_ + +#include "base/memory/raw_ptr.h" +#include "chrome/browser/profiles/profile.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/signin_investigator.h" + +// This version should work for anything with a profile object, like desktop and +// Android. +class InvestigatorDependencyProvider + : public SigninInvestigator::DependencyProvider { + public: + explicit InvestigatorDependencyProvider(Profile* profile); + + InvestigatorDependencyProvider(const InvestigatorDependencyProvider&) = + delete; + InvestigatorDependencyProvider& operator=( + const InvestigatorDependencyProvider&) = delete; + + ~InvestigatorDependencyProvider() override; + PrefService* GetPrefs() override; + + private: + // Non-owning pointer. + raw_ptr<Profile> profile_; +}; + +#endif // CHROME_BROWSER_SIGNIN_INVESTIGATOR_DEPENDENCY_PROVIDER_H_ diff --git a/chromium/chrome/browser/signin/logout_tab_helper.cc b/chromium/chrome/browser/signin/logout_tab_helper.cc new file mode 100644 index 00000000000..db560b21a69 --- /dev/null +++ b/chromium/chrome/browser/signin/logout_tab_helper.cc @@ -0,0 +1,34 @@ +// 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. + +#include "chrome/browser/signin/logout_tab_helper.h" + +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +WEB_CONTENTS_USER_DATA_KEY_IMPL(LogoutTabHelper); + +LogoutTabHelper::LogoutTabHelper(content::WebContents* web_contents) + : content::WebContentsUserData<LogoutTabHelper>(*web_contents), + content::WebContentsObserver(web_contents) {} + +LogoutTabHelper::~LogoutTabHelper() = default; + +void LogoutTabHelper::PrimaryPageChanged(content::Page& page) { + if (page.GetMainDocument().IsErrorDocument()) { + // Failed to load the logout page, fallback to local signout. + Profile* profile = + Profile::FromBrowserContext(web_contents()->GetBrowserContext()); + IdentityManagerFactory::GetForProfile(profile) + ->GetAccountsMutator() + ->RemoveAllAccounts(signin_metrics::SourceForRefreshTokenOperation:: + kLogoutTabHelper_PrimaryPageChanged); + } + + // Delete this. + web_contents()->RemoveUserData(UserDataKey()); +} diff --git a/chromium/chrome/browser/signin/logout_tab_helper.h b/chromium/chrome/browser/signin/logout_tab_helper.h new file mode 100644 index 00000000000..4f2974c4568 --- /dev/null +++ b/chromium/chrome/browser/signin/logout_tab_helper.h @@ -0,0 +1,36 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_ + +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" + +// Tab helper used for logout tabs. Monitors if the logout tab loaded correctly +// and fallbacks to local signout in case of failure. +// Only the first navigation is monitored. Even though the logout page sometimes +// redirects to the SAML provider through javascript, that second navigation is +// not monitored. The logout is considered successful if the first navigation +// succeeds, because the signout headers which cause the tokens to be revoked +// are there. +class LogoutTabHelper : public content::WebContentsUserData<LogoutTabHelper>, + public content::WebContentsObserver { + public: + ~LogoutTabHelper() override; + + LogoutTabHelper(const LogoutTabHelper&) = delete; + LogoutTabHelper& operator=(const LogoutTabHelper&) = delete; + + private: + friend class content::WebContentsUserData<LogoutTabHelper>; + explicit LogoutTabHelper(content::WebContents* web_contents); + + // content::WebContentsObserver: + void PrimaryPageChanged(content::Page& page) override; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); +}; + +#endif // CHROME_BROWSER_SIGNIN_LOGOUT_TAB_HELPER_H_ diff --git a/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc b/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc new file mode 100644 index 00000000000..7717ade215c --- /dev/null +++ b/chromium/chrome/browser/signin/logout_tab_helper_unittest.cc @@ -0,0 +1,24 @@ +// Copyright 2021 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. + +#include "chrome/browser/signin/logout_tab_helper.h" + +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "content/public/test/navigation_simulator.h" +#include "google_apis/gaia/gaia_urls.h" + +class LogoutTabHelperTest : public ChromeRenderViewHostTestHarness {}; + +TEST_F(LogoutTabHelperTest, SelfDeleteInPrimaryPageChanged) { + LogoutTabHelper::CreateForWebContents(web_contents()); + + EXPECT_NE(nullptr, LogoutTabHelper::FromWebContents(web_contents())); + + // Load the logout page. + content::NavigationSimulator::NavigateAndCommitFromBrowser( + web_contents(), GaiaUrls::GetInstance()->service_logout_url()); + + // The helper was deleted in PrimaryPageChanged. + EXPECT_EQ(nullptr, LogoutTabHelper::FromWebContents(web_contents())); +} diff --git a/chromium/chrome/browser/signin/mirror_browsertest.cc b/chromium/chrome/browser/signin/mirror_browsertest.cc new file mode 100644 index 00000000000..abc65d011e6 --- /dev/null +++ b/chromium/chrome/browser/signin/mirror_browsertest.cc @@ -0,0 +1,277 @@ +// 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. + +#include <map> +#include <memory> +#include <string> +#include <utility> + +#include "base/base_switches.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/command_line.h" +#include "base/path_service.h" +#include "base/run_loop.h" +#include "base/task/post_task.h" +#include "base/test/bind.h" +#include "build/build_config.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/chrome_content_browser_client.h" +#include "chrome/browser/extensions/api/identity/web_auth_flow.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/google/core/common/google_switches.h" +#include "components/network_session_configurator/common/network_switches.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/dice_header_helper.h" +#include "components/signin/core/browser/signin_header_helper.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "content/public/common/content_client.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/request_handler_util.h" +#include "third_party/blink/public/common/loader/url_loader_throttle.h" + +namespace { + +// A delegate to insert a user generated X-Chrome-Connected header +// to a specifict URL. +class HeaderModifyingThrottle : public blink::URLLoaderThrottle { + public: + HeaderModifyingThrottle() = default; + + HeaderModifyingThrottle(const HeaderModifyingThrottle&) = delete; + HeaderModifyingThrottle& operator=(const HeaderModifyingThrottle&) = delete; + + ~HeaderModifyingThrottle() override = default; + + void WillStartRequest(network::ResourceRequest* request, + bool* defer) override { + request->headers.SetHeader(signin::kChromeConnectedHeader, "User Data"); + } +}; + +class ThrottleContentBrowserClient : public ChromeContentBrowserClient { + public: + explicit ThrottleContentBrowserClient(const GURL& watch_url) + : watch_url_(watch_url) {} + + ThrottleContentBrowserClient(const ThrottleContentBrowserClient&) = delete; + ThrottleContentBrowserClient& operator=(const ThrottleContentBrowserClient&) = + delete; + + ~ThrottleContentBrowserClient() override = default; + + // ContentBrowserClient overrides: + std::vector<std::unique_ptr<blink::URLLoaderThrottle>> + CreateURLLoaderThrottles( + const network::ResourceRequest& request, + content::BrowserContext* browser_context, + const base::RepeatingCallback<content::WebContents*()>& wc_getter, + content::NavigationUIData* navigation_ui_data, + int frame_tree_node_id) override { + std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles; + if (request.url == watch_url_) + throttles.push_back(std::make_unique<HeaderModifyingThrottle>()); + return throttles; + } + + private: + const GURL watch_url_; +}; + +// Subclass of DiceManageAccountBrowserTest with Mirror enabled. +class MirrorBrowserTest : public InProcessBrowserTest { + protected: + void RunExtensionConsentTest(extensions::WebAuthFlow::Partition partition, + bool expects_header) { + net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); + https_server.AddDefaultHandlers(GetChromeTestDataDir()); + const std::string kAuthPath = "/auth"; + net::test_server::HttpRequest::HeaderMap headers; + base::RunLoop run_loop; + https_server.RegisterRequestMonitor(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) { + if (request.GetURL().path() != kAuthPath) + return; + + headers = request.headers; + run_loop.Quit(); + })); + ASSERT_TRUE(https_server.Start()); + + auto web_auth_flow = std::make_unique<extensions::WebAuthFlow>( + nullptr, browser()->profile(), + https_server.GetURL("google.com", kAuthPath), + extensions::WebAuthFlow::INTERACTIVE, partition); + + web_auth_flow->Start(); + run_loop.Run(); + EXPECT_EQ(!!headers.count(signin::kChromeConnectedHeader), expects_header); + + web_auth_flow.release()->DetachDelegateAndDelete(); + base::RunLoop().RunUntilIdle(); + } + + private: + void SetUpOnMainThread() override { + // The test makes requests to google.com and other domains which we want to + // redirect to the test server. + host_resolver()->AddRule("*", "127.0.0.1"); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + // HTTPS server only serves a valid cert for localhost, so this is needed to + // load pages from "www.google.com" without an interstitial. + command_line->AppendSwitch(switches::kIgnoreCertificateErrors); + + // The production code only allows known ports (80 for http and 443 for + // https), but the test server runs on a random port. + command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers); + } +}; + +// Verify the following items: +// 1- X-Chrome-Connected is appended on Google domains if account +// consistency is enabled and access is secure. +// 2- The header is stripped in case a request is redirected from a Gooogle +// domain to non-google domain. +// 3- The header is NOT stripped in case it is added directly by the page +// and not because it was on a secure Google domain. +// This is a regression test for crbug.com/588492. +IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, MirrorRequestHeader) { + browser()->profile()->GetPrefs()->SetString(prefs::kGoogleServicesAccountId, + "account_id"); + + base::Lock lock; + // Map from the path of the URLs that test server sees to the request header. + // This is the path, and not URL, because the requests use different domains + // which the mock HostResolver converts to 127.0.0.1. + std::map<std::string, net::test_server::HttpRequest::HeaderMap> header_map; + embedded_test_server()->RegisterRequestMonitor(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) { + base::AutoLock auto_lock(lock); + header_map[request.GetURL().path()] = request.headers; + })); + ASSERT_TRUE(embedded_test_server()->Start()); + + net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); + https_server.AddDefaultHandlers(GetChromeTestDataDir()); + https_server.RegisterRequestMonitor(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) { + base::AutoLock auto_lock(lock); + header_map[request.GetURL().path()] = request.headers; + })); + ASSERT_TRUE(https_server.Start()); + + base::FilePath root_http; + base::PathService::Get(chrome::DIR_TEST_DATA, &root_http); + root_http = root_http.AppendASCII("mirror_request_header"); + + struct TestCase { + GURL original_url; // The URL from which the request begins. + // The path to which navigation is redirected. + std::string redirected_to_path; + bool inject_header; // Should X-Chrome-Connected header be injected to the + // original request. + bool original_url_expects_header; // Expectation: The header should be + // visible in original URL. + bool redirected_to_url_expects_header; // Expectation: The header should be + // visible in redirected URL. + }; + + std::vector<TestCase> all_tests; + + // Neither should have the header. + // Note we need to replace the port of the redirect's URL. + base::StringPairs replacement_text; + replacement_text.push_back(std::make_pair( + "{{PORT}}", base::NumberToString(embedded_test_server()->port()))); + std::string replacement_path = net::test_server::GetFilePathWithReplacements( + "/mirror_request_header/http.www.google.com.html", replacement_text); + all_tests.push_back( + {embedded_test_server()->GetURL("www.google.com", replacement_path), + "/simple.html", false, false, false}); + + // First one adds the header and transfers it to the second. + replacement_path = net::test_server::GetFilePathWithReplacements( + "/mirror_request_header/http.www.header_adder.com.html", + replacement_text); + all_tests.push_back( + {embedded_test_server()->GetURL("www.header_adder.com", replacement_path), + "/simple.html", true, true, true}); + + // First one should have the header, but not transfered to second one. + replacement_text.clear(); + replacement_text.push_back( + std::make_pair("{{PORT}}", base::NumberToString(https_server.port()))); + replacement_path = net::test_server::GetFilePathWithReplacements( + "/mirror_request_header/https.www.google.com.html", replacement_text); + all_tests.push_back({https_server.GetURL("www.google.com", replacement_path), + "/simple.html", false, true, false}); + + for (const auto& test_case : all_tests) { + SCOPED_TRACE(test_case.original_url); + + // If test case requires adding header for the first url add a throttle. + ThrottleContentBrowserClient browser_client(test_case.original_url); + content::ContentBrowserClient* old_browser_client = nullptr; + if (test_case.inject_header) + old_browser_client = content::SetBrowserClientForTesting(&browser_client); + + // Navigate to first url. + ASSERT_TRUE( + ui_test_utils::NavigateToURL(browser(), test_case.original_url)); + + if (test_case.inject_header) + content::SetBrowserClientForTesting(old_browser_client); + + base::AutoLock auto_lock(lock); + + // Check if header exists and X-Chrome-Connected is correctly provided. + ASSERT_EQ(1u, header_map.count(test_case.original_url.path())); + if (test_case.original_url_expects_header) { + ASSERT_TRUE(header_map[test_case.original_url.path()].count( + signin::kChromeConnectedHeader)); + } else { + ASSERT_FALSE(header_map[test_case.original_url.path()].count( + signin::kChromeConnectedHeader)); + } + + ASSERT_EQ(1u, header_map.count(test_case.redirected_to_path)); + if (test_case.redirected_to_url_expects_header) { + ASSERT_TRUE(header_map[test_case.redirected_to_path].count( + signin::kChromeConnectedHeader)); + } else { + ASSERT_FALSE(header_map[test_case.redirected_to_path].count( + signin::kChromeConnectedHeader)); + } + + header_map.clear(); + } +} + +// Verifies that requests originated from chrome.identity.launchWebAuthFlow() +// API don't have Mirror headers attached. +// This is a regression test for crbug.com/1077504. +IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, + NoMirrorExtensionConsent_LaunchWebAuthFlow) { + RunExtensionConsentTest(extensions::WebAuthFlow::LAUNCH_WEB_AUTH_FLOW, false); +} + +// Verifies that requests originated from chrome.identity.getAuthToken() +// API have Mirror headers attached. +IN_PROC_BROWSER_TEST_F(MirrorBrowserTest, MirrorExtensionConsent_GetAuthToken) { + RunExtensionConsentTest(extensions::WebAuthFlow::GET_AUTH_TOKEN, true); +} + +} // namespace diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc new file mode 100644 index 00000000000..5669e179b48 --- /dev/null +++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.cc @@ -0,0 +1,146 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/process_dice_header_delegate_impl.h" + +#include <utility> + +#include "base/callback.h" +#include "base/logging.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/dice_tab_helper.h" +#include "chrome/browser/signin/dice_web_signin_interceptor.h" +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/webui/signin/signin_ui_error.h" +#include "chrome/common/url_constants.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/web_contents.h" +#include "url/gurl.h" + +namespace { + +void RedirectToNtp(content::WebContents* contents) { + VLOG(1) << "RedirectToNtp"; + contents->GetController().LoadURL( + GURL(chrome::kChromeUINewTabURL), content::Referrer(), + ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string()); +} + +// Helper function similar to DiceTabHelper::FromWebContents(), but also handles +// the case where |contents| is nullptr. +DiceTabHelper* GetDiceTabHelperFromWebContents(content::WebContents* contents) { + if (!contents) + return nullptr; + return DiceTabHelper::FromWebContents(contents); +} + +} // namespace + +ProcessDiceHeaderDelegateImpl::ProcessDiceHeaderDelegateImpl( + content::WebContents* web_contents, + EnableSyncCallback enable_sync_callback, + ShowSigninErrorCallback show_signin_error_callback) + : web_contents_(web_contents->GetWeakPtr()), + profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())), + enable_sync_callback_(std::move(enable_sync_callback)), + show_signin_error_callback_(std::move(show_signin_error_callback)) { + DCHECK(profile_); + + DiceTabHelper* tab_helper = DiceTabHelper::FromWebContents(web_contents); + if (tab_helper) { + is_sync_signin_tab_ = tab_helper->IsSyncSigninInProgress(); + redirect_url_ = tab_helper->redirect_url(); + access_point_ = tab_helper->signin_access_point(); + promo_action_ = tab_helper->signin_promo_action(); + reason_ = tab_helper->signin_reason(); + } +} + +ProcessDiceHeaderDelegateImpl::~ProcessDiceHeaderDelegateImpl() = default; + +bool ProcessDiceHeaderDelegateImpl::ShouldEnableSync() { + if (IdentityManagerFactory::GetForProfile(profile_)->HasPrimaryAccount( + signin::ConsentLevel::kSync)) { + VLOG(1) << "Do not start sync after web sign-in [already authenticated]."; + return false; + } + + if (!is_sync_signin_tab_) { + VLOG(1) + << "Do not start sync after web sign-in [not a Chrome sign-in tab]."; + return false; + } + + return true; +} + +void ProcessDiceHeaderDelegateImpl::HandleTokenExchangeSuccess( + CoreAccountId account_id, + bool is_new_account) { + // is_sync_signin_tab_ tells whether the current signin is happening in a tab + // that was opened from a "Enable Sync" Chrome UI. Usually this is indeed a + // sync signin, but it is not always the case: the user may abandon the sync + // signin and do a simple web signin in the same tab instead. + DiceWebSigninInterceptorFactory::GetForProfile(profile_) + ->MaybeInterceptWebSignin(web_contents_.get(), account_id, is_new_account, + is_sync_signin_tab_); +} + +void ProcessDiceHeaderDelegateImpl::EnableSync( + const CoreAccountId& account_id) { + DiceTabHelper* tab_helper = + GetDiceTabHelperFromWebContents(web_contents_.get()); + if (tab_helper) + tab_helper->OnSyncSigninFlowComplete(); + + if (!ShouldEnableSync()) { + // No special treatment is needed if the user is not enabling sync. + return; + } + + content::WebContents* web_contents = web_contents_.get(); + VLOG(1) << "Start sync after web sign-in."; + std::move(enable_sync_callback_) + .Run(profile_.get(), access_point_, promo_action_, reason_, web_contents, + account_id); + + if (!web_contents) + return; + + // After signing in to Chrome, the user should be redirected to the NTP, + // unless specified otherwise. + if (redirect_url_.is_empty()) { + RedirectToNtp(web_contents); + return; + } + + DCHECK(redirect_url_.is_valid()); + web_contents->GetController().LoadURL(redirect_url_, content::Referrer(), + ui::PAGE_TRANSITION_AUTO_TOPLEVEL, + std::string()); +} + +void ProcessDiceHeaderDelegateImpl::HandleTokenExchangeFailure( + const std::string& email, + const GoogleServiceAuthError& error) { + DCHECK_NE(GoogleServiceAuthError::NONE, error.state()); + DiceTabHelper* tab_helper = + GetDiceTabHelperFromWebContents(web_contents_.get()); + if (tab_helper) + tab_helper->OnSyncSigninFlowComplete(); + + bool should_enable_sync = ShouldEnableSync(); + + content::WebContents* web_contents = web_contents_.get(); + if (should_enable_sync && web_contents) + RedirectToNtp(web_contents); + + // Show the error even if the WebContents was closed, because the user may be + // signed out of the web. + std::move(show_signin_error_callback_) + .Run(profile_.get(), web_contents, + SigninUIError::FromGoogleServiceAuthError(email, error)); +} diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h new file mode 100644 index 00000000000..cd7116700c6 --- /dev/null +++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl.h @@ -0,0 +1,77 @@ +// Copyright 2017 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. + +#ifndef CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_ +#define CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_ + +#include "base/memory/raw_ptr.h" +#include "chrome/browser/signin/dice_response_handler.h" + +#include <memory> +#include <string> + +#include "base/callback_forward.h" +#include "base/memory/weak_ptr.h" +#include "components/signin/public/base/signin_metrics.h" + +namespace content { +class WebContents; +} + +class Profile; +class SigninUIError; + +class ProcessDiceHeaderDelegateImpl : public ProcessDiceHeaderDelegate { + public: + // Callback starting Sync. + using EnableSyncCallback = + base::OnceCallback<void(Profile*, + signin_metrics::AccessPoint, + signin_metrics::PromoAction, + signin_metrics::Reason, + content::WebContents*, + const CoreAccountId&)>; + + // Callback showing a signin error UI. + using ShowSigninErrorCallback = base::OnceCallback< + void(Profile*, content::WebContents*, const SigninUIError&)>; + + // |is_sync_signin_tab| is true if a sync signin flow has been started in that + // tab. + ProcessDiceHeaderDelegateImpl( + content::WebContents* web_contents, + EnableSyncCallback enable_sync_callback, + ShowSigninErrorCallback show_signin_error_callback); + + ProcessDiceHeaderDelegateImpl(const ProcessDiceHeaderDelegateImpl&) = delete; + ProcessDiceHeaderDelegateImpl& operator=( + const ProcessDiceHeaderDelegateImpl&) = delete; + + ~ProcessDiceHeaderDelegateImpl() override; + + // ProcessDiceHeaderDelegate: + void HandleTokenExchangeSuccess(CoreAccountId account_id, + bool is_new_account) override; + void EnableSync(const CoreAccountId& account_id) override; + void HandleTokenExchangeFailure(const std::string& email, + const GoogleServiceAuthError& error) override; + + private: + // Returns true if sync should be enabled after the user signs in. + bool ShouldEnableSync(); + + const base::WeakPtr<content::WebContents> web_contents_; + raw_ptr<Profile> profile_; + EnableSyncCallback enable_sync_callback_; + ShowSigninErrorCallback show_signin_error_callback_; + bool is_sync_signin_tab_ = false; + signin_metrics::AccessPoint access_point_ = + signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN; + signin_metrics::PromoAction promo_action_ = + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO; + signin_metrics::Reason reason_ = signin_metrics::Reason::kUnknownReason; + GURL redirect_url_; +}; + +#endif // CHROME_BROWSER_SIGNIN_PROCESS_DICE_HEADER_DELEGATE_IMPL_H_ diff --git a/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc b/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc new file mode 100644 index 00000000000..60c842301f9 --- /dev/null +++ b/chromium/chrome/browser/signin/process_dice_header_delegate_impl_unittest.cc @@ -0,0 +1,375 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/process_dice_header_delegate_impl.h" + +#include <memory> +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/signin/dice_tab_helper.h" +#include "chrome/browser/signin/dice_web_signin_interceptor.h" +#include "chrome/browser/signin/dice_web_signin_interceptor_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/ui/webui/signin/signin_ui_error.h" +#include "chrome/common/url_constants.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "components/signin/public/base/account_consistency_method.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/navigation_simulator.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using signin_metrics::Reason; + +namespace { + +signin_metrics::AccessPoint kTestAccessPoint = + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE; + +signin_metrics::PromoAction kTestPromoAction = + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO; + +// Dummy delegate that declines all interceptions. +class TestDiceWebSigninInterceptorDelegate + : public DiceWebSigninInterceptor::Delegate { + public: + ~TestDiceWebSigninInterceptorDelegate() override = default; + std::unique_ptr<ScopedDiceWebSigninInterceptionBubbleHandle> + ShowSigninInterceptionBubble( + content::WebContents* web_contents, + const BubbleParameters& bubble_parameters, + base::OnceCallback<void(SigninInterceptionResult)> callback) override { + std::move(callback).Run(SigninInterceptionResult::kDeclined); + return nullptr; + } + + void ShowProfileCustomizationBubble(Browser* browser) override {} +}; + +class MockDiceWebSigninInterceptor : public DiceWebSigninInterceptor { + public: + explicit MockDiceWebSigninInterceptor(Profile* profile) + : DiceWebSigninInterceptor( + profile, + std::make_unique<TestDiceWebSigninInterceptorDelegate>()) {} + ~MockDiceWebSigninInterceptor() override = default; + + MOCK_METHOD(void, + MaybeInterceptWebSignin, + (content::WebContents * web_contents, + CoreAccountId account_id, + bool is_new_account, + bool is_sync_signin), + (override)); +}; + +std::unique_ptr<KeyedService> CreateMockDiceWebSigninInterceptor( + content::BrowserContext* context) { + return std::make_unique<MockDiceWebSigninInterceptor>( + Profile::FromBrowserContext(context)); +} + +class ProcessDiceHeaderDelegateImplTest + : public ChromeRenderViewHostTestHarness { + public: + ProcessDiceHeaderDelegateImplTest() + : enable_sync_called_(false), + show_error_called_(false), + account_id_("12345"), + email_("foo@bar.com"), + auth_error_(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) {} + + ~ProcessDiceHeaderDelegateImplTest() override {} + + void AddAccount(bool is_primary) { + if (!identity_test_environment_profile_adaptor_) + InitializeIdentityTestEnvironment(); + if (is_primary) { + identity_test_environment_profile_adaptor_->identity_test_env() + ->SetPrimaryAccount(email_, signin::ConsentLevel::kSync); + } else { + identity_test_environment_profile_adaptor_->identity_test_env() + ->MakeAccountAvailable(email_); + } + } + + void InitializeIdentityTestEnvironment() { + DCHECK(profile()); + identity_test_environment_profile_adaptor_ = + std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile()); + } + + // Creates a ProcessDiceHeaderDelegateImpl instance. + std::unique_ptr<ProcessDiceHeaderDelegateImpl> + CreateDelegateAndNavigateToSignin( + bool is_sync_signin_tab, + Reason reason = Reason::kSigninPrimaryAccount) { + signin_reason_ = reason; + if (!identity_test_environment_profile_adaptor_) + InitializeIdentityTestEnvironment(); + // Load the signin page. + std::unique_ptr<content::NavigationSimulator> simulator = + content::NavigationSimulator::CreateRendererInitiated(signin_url_, + main_rfh()); + simulator->Start(); + if (is_sync_signin_tab) { + DiceTabHelper::CreateForWebContents(web_contents()); + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + dice_tab_helper->InitializeSigninFlow(signin_url_, kTestAccessPoint, + signin_reason_, kTestPromoAction, + GURL::EmptyGURL()); + } + simulator->Commit(); + DCHECK_EQ(signin_url_, web_contents()->GetVisibleURL()); + return std::make_unique<ProcessDiceHeaderDelegateImpl>( + web_contents(), + base::BindOnce(&ProcessDiceHeaderDelegateImplTest::StartSyncCallback, + base::Unretained(this)), + base::BindOnce( + &ProcessDiceHeaderDelegateImplTest::ShowSigninErrorCallback, + base::Unretained(this))); + } + + // ChromeRenderViewHostTestHarness: + TestingProfile::TestingFactories GetTestingFactories() const override { + TestingProfile::TestingFactories factories = { + {DiceWebSigninInterceptorFactory::GetInstance(), + base::BindRepeating(&CreateMockDiceWebSigninInterceptor)}}; + IdentityTestEnvironmentProfileAdaptor:: + AppendIdentityTestEnvironmentFactories(&factories); + return factories; + } + + void TearDown() override { + identity_test_environment_profile_adaptor_.reset(); + ChromeRenderViewHostTestHarness::TearDown(); + } + + // Callback for the ProcessDiceHeaderDelegateImpl. + void StartSyncCallback(Profile* profile, + signin_metrics::AccessPoint access_point, + signin_metrics::PromoAction promo_action, + signin_metrics::Reason reason, + content::WebContents* contents, + const CoreAccountId& account_id) { + EXPECT_EQ(profile, this->profile()); + EXPECT_EQ(access_point, kTestAccessPoint); + EXPECT_EQ(promo_action, kTestPromoAction); + EXPECT_EQ(reason, signin_reason_); + EXPECT_EQ(web_contents(), contents); + EXPECT_EQ(account_id_, account_id); + enable_sync_called_ = true; + } + + // Callback for the ProcessDiceHeaderDelegateImpl. + void ShowSigninErrorCallback(Profile* profile, + content::WebContents* contents, + const SigninUIError& error) { + EXPECT_EQ(profile, this->profile()); + EXPECT_EQ(web_contents(), contents); + EXPECT_EQ(base::UTF8ToUTF16(auth_error_.ToString()), error.message()); + EXPECT_EQ(base::UTF8ToUTF16(email_), error.email()); + show_error_called_ = true; + } + + MockDiceWebSigninInterceptor* mock_interceptor() { + return static_cast<MockDiceWebSigninInterceptor*>( + DiceWebSigninInterceptorFactory::GetForProfile(profile())); + } + + std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> + identity_test_environment_profile_adaptor_; + + const GURL signin_url_ = GURL("https://accounts.google.com"); + bool enable_sync_called_; + bool show_error_called_; + CoreAccountId account_id_; + std::string email_; + GoogleServiceAuthError auth_error_; + Reason signin_reason_ = Reason::kSigninPrimaryAccount; +}; + +// Check that sync is enabled if the tab is closed during signin. +TEST_F(ProcessDiceHeaderDelegateImplTest, CloseTabWhileStartingSync) { + std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate = + CreateDelegateAndNavigateToSignin(true); + + // Close the tab. + DeleteContents(); + + // Check expectations. + delegate->EnableSync(account_id_); + EXPECT_TRUE(enable_sync_called_); + EXPECT_FALSE(show_error_called_); +} + +// Check that the error is still shown if the tab is closed before the error is +// received. +TEST_F(ProcessDiceHeaderDelegateImplTest, CloseTabWhileFailingSignin) { + std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate = + CreateDelegateAndNavigateToSignin(true); + + // Close the tab. + DeleteContents(); + + // Check expectations. + delegate->HandleTokenExchangeFailure(email_, auth_error_); + EXPECT_FALSE(enable_sync_called_); + EXPECT_TRUE(show_error_called_); +} + +struct TestConfiguration { + // Test setup. + bool signed_in; // User was already signed in at the start of the flow. + bool signin_tab; // The tab is marked as a Sync signin tab. + + // Test expectations. + bool callback_called; // The relevant callback was called. + bool show_ntp; // The NTP was shown. +}; + +TestConfiguration kEnableSyncTestCases[] = { + // clang-format off + // signed_in | signin_tab | callback_called | show_ntp + { false, false, false, false}, + { false, true, true, true}, + { true, false, false, false}, + { true, true, false, false}, + // clang-format on +}; + +// Parameterized version of ProcessDiceHeaderDelegateImplTest. +class ProcessDiceHeaderDelegateImplTestEnableSync + : public ProcessDiceHeaderDelegateImplTest, + public ::testing::WithParamInterface<TestConfiguration> {}; + +// Test the EnableSync() method in all configurations. +TEST_P(ProcessDiceHeaderDelegateImplTestEnableSync, EnableSync) { + if (GetParam().signed_in) + AddAccount(/*is_primary=*/true); + std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate = + CreateDelegateAndNavigateToSignin(GetParam().signin_tab); + delegate->EnableSync(account_id_); + EXPECT_EQ(GetParam().callback_called, enable_sync_called_); + GURL expected_url = + GetParam().show_ntp ? GURL(chrome::kChromeUINewTabURL) : signin_url_; + EXPECT_EQ(expected_url, web_contents()->GetVisibleURL()); + EXPECT_FALSE(show_error_called_); + // Check that the sync signin flow is complete. + if (GetParam().signin_tab) { + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + ASSERT_TRUE(dice_tab_helper); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); + } +} + +INSTANTIATE_TEST_SUITE_P(All, + ProcessDiceHeaderDelegateImplTestEnableSync, + ::testing::ValuesIn(kEnableSyncTestCases)); + +TestConfiguration kHandleTokenExchangeFailureTestCases[] = { + // clang-format off + // signed_in | signin_tab | callback_called | show_ntp + { false, false, true, false}, + { false, true, true, true}, + { true, false, true, false}, + { true, true, true, false}, + // clang-format on +}; + +// Parameterized version of ProcessDiceHeaderDelegateImplTest. +class ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure + : public ProcessDiceHeaderDelegateImplTest, + public ::testing::WithParamInterface<TestConfiguration> {}; + +// Test the HandleTokenExchangeFailure() method in all configurations. +TEST_P(ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure, + HandleTokenExchangeFailure) { + if (GetParam().signed_in) + AddAccount(/*is_primary=*/true); + std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate = + CreateDelegateAndNavigateToSignin(GetParam().signin_tab); + delegate->HandleTokenExchangeFailure(email_, auth_error_); + EXPECT_FALSE(enable_sync_called_); + EXPECT_EQ(GetParam().callback_called, show_error_called_); + GURL expected_url = + GetParam().show_ntp ? GURL(chrome::kChromeUINewTabURL) : signin_url_; + EXPECT_EQ(expected_url, web_contents()->GetVisibleURL()); + // Check that the sync signin flow is complete. + if (GetParam().signin_tab) { + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + ASSERT_TRUE(dice_tab_helper); + EXPECT_FALSE(dice_tab_helper->IsSyncSigninInProgress()); + } +} + +INSTANTIATE_TEST_SUITE_P( + All, + ProcessDiceHeaderDelegateImplTestHandleTokenExchangeFailure, + ::testing::ValuesIn(kHandleTokenExchangeFailureTestCases)); + +struct TokenExchangeSuccessConfiguration { + bool is_reauth; // User was already signed in with the account. + bool signin_tab; // A DiceTabHelper is attached to the tab. + Reason reason; + bool sync_signin; // Expected value for the MaybeInterceptWebSigin call. +}; + +TokenExchangeSuccessConfiguration kHandleTokenExchangeSuccessTestCases[] = { + // clang-format off + // is_reauth | signin_tab | reason   | sync_signin + { false, false, Reason::kSigninPrimaryAccount, false }, + { false, true, Reason::kSigninPrimaryAccount, true }, + { false, true, Reason::kAddSecondaryAccount, false }, + { true, false, Reason::kSigninPrimaryAccount, false }, + { true, true, Reason::kSigninPrimaryAccount, true }, + // clang-format on +}; + +// Parameterized version of ProcessDiceHeaderDelegateImplTest. +class ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess + : public ProcessDiceHeaderDelegateImplTest, + public ::testing::WithParamInterface<TokenExchangeSuccessConfiguration> { +}; + +// Test the HandleTokenExchangeSuccess() method in all configurations. +TEST_P(ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess, + HandleTokenExchangeSuccess) { + if (GetParam().is_reauth) + AddAccount(/*is_primary=*/false); + std::unique_ptr<ProcessDiceHeaderDelegateImpl> delegate = + CreateDelegateAndNavigateToSignin(GetParam().signin_tab, + GetParam().reason); + EXPECT_CALL( + *mock_interceptor(), + MaybeInterceptWebSignin(web_contents(), account_id_, + !GetParam().is_reauth, GetParam().sync_signin)); + delegate->HandleTokenExchangeSuccess(account_id_, !GetParam().is_reauth); + + // Check that the sync signin flow is complete. + if (GetParam().signin_tab) { + DiceTabHelper* dice_tab_helper = + DiceTabHelper::FromWebContents(web_contents()); + ASSERT_TRUE(dice_tab_helper); + EXPECT_EQ(GetParam().sync_signin, + dice_tab_helper->IsSyncSigninInProgress()); + } +} + +INSTANTIATE_TEST_SUITE_P( + All, + ProcessDiceHeaderDelegateImplTestHandleTokenExchangeSuccess, + ::testing::ValuesIn(kHandleTokenExchangeSuccessTestCases)); + +} // namespace diff --git a/chromium/chrome/browser/signin/reauth_result.h b/chromium/chrome/browser/signin/reauth_result.h new file mode 100644 index 00000000000..9aa85bed81c --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_result.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_ +#define CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_ + +namespace signin { + +// Indicates the result of the Gaia Reauth flow. +// Needs to be kept in sync with "SigninReauthResult" in enums.xml. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class ReauthResult { + // The user was successfully re-authenticated. + kSuccess = 0, + + // The user account is not signed in. + kAccountNotSignedIn = 1, + + // The user dismissed the reauth prompt. + kDismissedByUser = 2, + + // The reauth page failed to load. + kLoadFailed = 3, + + // A caller canceled the reauth flow. + kCancelled = 4, + + // An unexpected response was received from Gaia. + kUnexpectedResponse = 5, + + kMaxValue = kUnexpectedResponse, +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_REAUTH_RESULT_H_ diff --git a/chromium/chrome/browser/signin/reauth_tab_helper.cc b/chromium/chrome/browser/signin/reauth_tab_helper.cc new file mode 100644 index 00000000000..e8bd1d8b1c8 --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_tab_helper.cc @@ -0,0 +1,96 @@ +// 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. + +#include "chrome/browser/signin/reauth_tab_helper.h" +#include "base/memory/ptr_util.h" +#include "chrome/browser/signin/reauth_result.h" +#include "content/public/browser/navigation_handle.h" +#include "net/http/http_status_code.h" +#include "url/origin.h" + +namespace signin { + +namespace { + +bool IsExpectedResponseCode(int response_code) { + return response_code == net::HTTP_OK || response_code == net::HTTP_NO_CONTENT; +} + +} // namespace + +// static +void ReauthTabHelper::CreateForWebContents(content::WebContents* web_contents, + const GURL& reauth_url, + ReauthCallback callback) { + DCHECK(web_contents); + if (!FromWebContents(web_contents)) { + web_contents->SetUserData( + UserDataKey(), base::WrapUnique(new ReauthTabHelper( + web_contents, reauth_url, std::move(callback)))); + } else { + std::move(callback).Run(signin::ReauthResult::kCancelled); + } +} + +ReauthTabHelper::~ReauthTabHelper() = default; + +void ReauthTabHelper::CompleteReauth(signin::ReauthResult result) { + if (callback_) + std::move(callback_).Run(result); +} + +void ReauthTabHelper::DidFinishNavigation( + content::NavigationHandle* navigation_handle) { + if (!navigation_handle->IsInPrimaryMainFrame()) + return; + + is_within_reauth_origin_ &= + url::IsSameOriginWith(reauth_url_, navigation_handle->GetURL()); + + if (navigation_handle->IsErrorPage()) { + has_last_committed_error_page_ = true; + return; + } + + has_last_committed_error_page_ = false; + + GURL::Replacements replacements; + replacements.ClearQuery(); + GURL url_without_query = + navigation_handle->GetURL().ReplaceComponents(replacements); + if (url_without_query != reauth_url_) + return; + + if (!navigation_handle->GetResponseHeaders() || + !IsExpectedResponseCode( + navigation_handle->GetResponseHeaders()->response_code())) { + CompleteReauth(signin::ReauthResult::kUnexpectedResponse); + } + + CompleteReauth(signin::ReauthResult::kSuccess); +} + +void ReauthTabHelper::WebContentsDestroyed() { + CompleteReauth(signin::ReauthResult::kDismissedByUser); +} + +bool ReauthTabHelper::is_within_reauth_origin() { + return is_within_reauth_origin_; +} + +bool ReauthTabHelper::has_last_committed_error_page() { + return has_last_committed_error_page_; +} + +ReauthTabHelper::ReauthTabHelper(content::WebContents* web_contents, + const GURL& reauth_url, + ReauthCallback callback) + : content::WebContentsUserData<ReauthTabHelper>(*web_contents), + content::WebContentsObserver(web_contents), + reauth_url_(reauth_url), + callback_(std::move(callback)) {} + +WEB_CONTENTS_USER_DATA_KEY_IMPL(ReauthTabHelper); + +} // namespace signin diff --git a/chromium/chrome/browser/signin/reauth_tab_helper.h b/chromium/chrome/browser/signin/reauth_tab_helper.h new file mode 100644 index 00000000000..f830c24caec --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_tab_helper.h @@ -0,0 +1,66 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_ +#define CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_ + +#include "base/callback.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" +#include "url/gurl.h" + +namespace signin { + +enum class ReauthResult; + +// Tab helper class observing navigations within the reauth flow and notifying +// a caller about a flow result. +class ReauthTabHelper : public content::WebContentsUserData<ReauthTabHelper>, + public content::WebContentsObserver { + public: + using ReauthCallback = base::OnceCallback<void(signin::ReauthResult)>; + + // Creates a new ReauthTabHelper and attaches it to |web_contents|. If an + // instance is already attached, no replacement happens, just notifies the + // caller by invoking |callback| with signin::ReauthResult::kCancelled. + // Initializes a helper with: + // - |callback| to be called when the reauth flow is complete. + // - |reauth_url| that should be the final destination of the reauth flow. + static void CreateForWebContents(content::WebContents* web_contents, + const GURL& reauth_url, + ReauthCallback callback); + + ReauthTabHelper(const ReauthTabHelper&) = delete; + ReauthTabHelper& operator=(const ReauthTabHelper&) = delete; + + ~ReauthTabHelper() override; + + // If |callback_| is not null, calls |callback_| with |result|. + void CompleteReauth(signin::ReauthResult result); + + // content::WebContentsObserver + void DidFinishNavigation( + content::NavigationHandle* navigation_handle) override; + void WebContentsDestroyed() override; + + bool is_within_reauth_origin(); + bool has_last_committed_error_page(); + + private: + friend class content::WebContentsUserData<ReauthTabHelper>; + explicit ReauthTabHelper(content::WebContents* web_contents, + const GURL& reauth_url, + ReauthCallback callback); + + const GURL reauth_url_; + ReauthCallback callback_; + bool is_within_reauth_origin_ = true; + bool has_last_committed_error_page_ = false; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); +}; + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_REAUTH_TAB_HELPER_H_ diff --git a/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc b/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc new file mode 100644 index 00000000000..a17078cb067 --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_tab_helper_unittest.cc @@ -0,0 +1,184 @@ +// 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. + +#include "chrome/browser/signin/reauth_tab_helper.h" + +#include "base/memory/raw_ptr.h" +#include "base/test/mock_callback.h" +#include "chrome/browser/signin/reauth_result.h" +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "content/public/test/navigation_simulator.h" +#include "content/public/test/web_contents_tester.h" +#include "net/base/net_errors.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/features.h" + +namespace signin { + +class ReauthTabHelperTest : public ChromeRenderViewHostTestHarness { + public: + ReauthTabHelperTest() + : reauth_url_("https://my-identity_provider.com/reauth") {} + + void SetUp() override { + ChromeRenderViewHostTestHarness::SetUp(); + + ReauthTabHelper::CreateForWebContents(web_contents(), reauth_url(), + mock_callback_.Get()); + tab_helper_ = ReauthTabHelper::FromWebContents(web_contents()); + } + + ReauthTabHelper* tab_helper() { return tab_helper_; } + + base::MockOnceCallback<void(signin::ReauthResult)>* mock_callback() { + return &mock_callback_; + } + + const GURL& reauth_url() { return reauth_url_; } + + private: + raw_ptr<ReauthTabHelper> tab_helper_ = nullptr; + base::MockOnceCallback<void(signin::ReauthResult)> mock_callback_; + const GURL reauth_url_; +}; + +// Tests a direct call to CompleteReauth(). +TEST_F(ReauthTabHelperTest, CompleteReauth) { + signin::ReauthResult result = signin::ReauthResult::kSuccess; + EXPECT_CALL(*mock_callback(), Run(result)); + tab_helper()->CompleteReauth(result); +} + +// Tests a successful navigation to the reauth URL. +TEST_F(ReauthTabHelperTest, NavigateToReauthURL) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator->Commit(); +} + +// Tests the reauth flow when the reauth URL has query parameters. +TEST_F(ReauthTabHelperTest, NavigateToReauthURLWithQuery) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url().Resolve("?rapt=35be36ae"), web_contents()); + simulator->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator->Commit(); +} + +// Tests the reauth flow with multiple navigations within the same origin. +TEST_F(ReauthTabHelperTest, MultipleNavigationReauth) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + simulator->Redirect( + reauth_url().DeprecatedGetOriginAsURL().Resolve("/login")); + simulator->Commit(); + + auto simulator2 = content::NavigationSimulator::CreateRendererInitiated( + reauth_url(), main_rfh()); + simulator2->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator2->Commit(); +} + +// Tests the reauth flow with multiple navigations across two different origins. +// TODO(https://crbug.com/1045515): update this test once navigations outside of +// reauth_url() are blocked. +TEST_F(ReauthTabHelperTest, MultipleNavigationReauthThroughExternalOrigin) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + simulator->Redirect(GURL("https://other-identity-provider.com/login")); + simulator->Commit(); + + auto simulator2 = content::NavigationSimulator::CreateRendererInitiated( + reauth_url(), main_rfh()); + simulator2->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator2->Commit(); +} + +// Tests a failed navigation to the reauth URL, followed by a successful +// navigation. +TEST_F(ReauthTabHelperTest, NavigationToReauthURLFailed) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + simulator->Fail(net::ERR_TIMED_OUT); + simulator->CommitErrorPage(); + EXPECT_TRUE(tab_helper()->has_last_committed_error_page()); + // Check that the navigation still counts as within the same origin. + EXPECT_TRUE(tab_helper()->is_within_reauth_origin()); + + auto simulator2 = content::NavigationSimulator::CreateRendererInitiated( + reauth_url(), main_rfh()); + simulator2->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator2->Commit(); + EXPECT_FALSE(tab_helper()->has_last_committed_error_page()); +} + +// Tests a failed navigation redirecting to an external origin, followed by a +// successful navigation. +TEST_F(ReauthTabHelperTest, NavigationToExternalOriginFailed) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + reauth_url(), web_contents()); + simulator->Start(); + simulator->Redirect(GURL("https://other-identity-provider.com/login")); + simulator->Fail(net::ERR_TIMED_OUT); + simulator->CommitErrorPage(); + EXPECT_TRUE(tab_helper()->has_last_committed_error_page()); + // Check that the navigation doesn't count as within the same origin. + EXPECT_FALSE(tab_helper()->is_within_reauth_origin()); + + auto simulator2 = content::NavigationSimulator::CreateRendererInitiated( + reauth_url(), main_rfh()); + simulator2->Start(); + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kSuccess)); + simulator2->Commit(); + EXPECT_FALSE(tab_helper()->has_last_committed_error_page()); + EXPECT_FALSE(tab_helper()->is_within_reauth_origin()); +} + +// Tests the WebContents deletion. +TEST_F(ReauthTabHelperTest, WebContentsDestroyed) { + EXPECT_CALL(*mock_callback(), Run(signin::ReauthResult::kDismissedByUser)); + DeleteContents(); +} + +class ReauthTabHelperPrerenderTest : public ReauthTabHelperTest { + public: + ReauthTabHelperPrerenderTest() { + feature_list_.InitWithFeatures( + {blink::features::kPrerender2}, + // Disable the memory requirement of Prerender2 so the test can run on + // any bot. + {blink::features::kPrerender2MemoryControls}); + } + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(ReauthTabHelperPrerenderTest, + PrerenderDoesNotAffectLastCommittedErrorPage) { + content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(), + reauth_url()); + EXPECT_FALSE(tab_helper()->has_last_committed_error_page()); + + // Fail prerendering navigation. + const GURL prerender_url = reauth_url().Resolve("?prerendering"); + auto simulator = content::WebContentsTester::For(web_contents()) + ->AddPrerenderAndStartNavigation(prerender_url); + simulator->Fail(net::ERR_TIMED_OUT); + simulator->CommitErrorPage(); + + // has_last_committed_error_page_ is not updated by preredering. + EXPECT_FALSE(tab_helper()->has_last_committed_error_page()); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/reauth_util.cc b/chromium/chrome/browser/signin/reauth_util.cc new file mode 100644 index 00000000000..b770da50fb0 --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_util.cc @@ -0,0 +1,40 @@ +// 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. + +#include <string> + +#include "base/strings/string_number_conversions.h" +#include "chrome/browser/signin/reauth_util.h" +#include "chrome/common/webui_url_constants.h" +#include "net/base/url_util.h" + +namespace signin { + +GURL GetReauthConfirmationURL(signin_metrics::ReauthAccessPoint access_point) { + GURL url = GURL(chrome::kChromeUISigninReauthURL); + url = net::AppendQueryParameter( + url, "access_point", + base::NumberToString(static_cast<int>(access_point))); + return url; +} + +signin_metrics::ReauthAccessPoint GetReauthAccessPointForReauthConfirmationURL( + const GURL& url) { + std::string value; + if (!net::GetValueForKeyInQuery(url, "access_point", &value)) + return signin_metrics::ReauthAccessPoint::kUnknown; + + int access_point = -1; + base::StringToInt(value, &access_point); + if (access_point <= + static_cast<int>(signin_metrics::ReauthAccessPoint::kUnknown) || + access_point > + static_cast<int>(signin_metrics::ReauthAccessPoint::kMaxValue)) { + return signin_metrics::ReauthAccessPoint::kUnknown; + } + + return static_cast<signin_metrics::ReauthAccessPoint>(access_point); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/reauth_util.h b/chromium/chrome/browser/signin/reauth_util.h new file mode 100644 index 00000000000..52150ee53f1 --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_util.h @@ -0,0 +1,24 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_ + +#include "components/signin/public/base/signin_metrics.h" +#include "url/gurl.h" + +namespace signin { + +// Returns a URL to display in the reauth confirmation dialog. The dialog was +// triggered by |access_point|. +GURL GetReauthConfirmationURL(signin_metrics::ReauthAccessPoint access_point); + +// Returns ReauthAccessPoint encoded in the query of the reauth confirmation +// URL. +signin_metrics::ReauthAccessPoint GetReauthAccessPointForReauthConfirmationURL( + const GURL& url); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_REAUTH_UTIL_H_ diff --git a/chromium/chrome/browser/signin/reauth_util_unittest.cc b/chromium/chrome/browser/signin/reauth_util_unittest.cc new file mode 100644 index 00000000000..44fb952465d --- /dev/null +++ b/chromium/chrome/browser/signin/reauth_util_unittest.cc @@ -0,0 +1,33 @@ +// 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. + +#include "chrome/browser/signin/reauth_util.h" + +#include "chrome/common/webui_url_constants.h" +#include "components/signin/public/base/signin_metrics.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace signin { + +class ReauthUtilURLTest : public ::testing::TestWithParam<int> {}; + +TEST_P(ReauthUtilURLTest, GetAndParseReauthConfirmationURL) { + auto access_point = + static_cast<signin_metrics::ReauthAccessPoint>(GetParam()); + GURL url = GetReauthConfirmationURL(access_point); + ASSERT_TRUE(url.is_valid()); + EXPECT_EQ(url.host(), chrome::kChromeUISigninReauthHost); + signin_metrics::ReauthAccessPoint get_access_point = + GetReauthAccessPointForReauthConfirmationURL(url); + EXPECT_EQ(get_access_point, access_point); +} + +INSTANTIATE_TEST_CASE_P( + AllAccessPoints, + ReauthUtilURLTest, + ::testing::Range( + static_cast<int>(signin_metrics::ReauthAccessPoint::kUnknown), + static_cast<int>(signin_metrics::ReauthAccessPoint::kMaxValue) + 1)); + +} // namespace signin diff --git a/chromium/chrome/browser/signin/remove_local_account_browsertest.cc b/chromium/chrome/browser/signin/remove_local_account_browsertest.cc new file mode 100644 index 00000000000..259f3d2857e --- /dev/null +++ b/chromium/chrome/browser/signin/remove_local_account_browsertest.cc @@ -0,0 +1,143 @@ +// Copyright 2021 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. + +#include <memory> + +#include "base/run_loop.h" +#include "base/test/bind.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/test/base/mixin_based_in_process_browser_test.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/signin/public/identity_manager/test_identity_manager_observer.h" +#include "content/public/test/browser_test.h" +#include "google_apis/gaia/fake_gaia.h" +#include "google_apis/gaia/gaia_switches.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_response.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/login/test/network_portal_detector_mixin.h" +#endif + +namespace { + +using testing::Contains; +using testing::Not; + +MATCHER_P(ListedAccountMatchesGaiaId, gaia_id, "") { + return arg.gaia_id == std::string(gaia_id); +} + +const char kTestGaiaId[] = "123"; + +class RemoveLocalAccountTest : public MixinBasedInProcessBrowserTest { + protected: + RemoveLocalAccountTest() + : embedded_test_server_(net::EmbeddedTestServer::TYPE_HTTPS) { + embedded_test_server_.RegisterRequestHandler(base::BindRepeating( + &FakeGaia::HandleRequest, base::Unretained(&fake_gaia_))); + } + + ~RemoveLocalAccountTest() override = default; + + signin::IdentityManager* identity_manager() { + return IdentityManagerFactory::GetForProfile(browser()->profile()); + } + + signin::AccountsInCookieJarInfo WaitUntilAccountsInCookieUpdated() { + signin::TestIdentityManagerObserver observer(identity_manager()); + base::RunLoop run_loop; + observer.SetOnAccountsInCookieUpdatedCallback(run_loop.QuitClosure()); + run_loop.Run(); + return observer.AccountsInfoFromAccountsInCookieUpdatedCallback(); + } + + // MixinBasedInProcessBrowserTest: + void SetUpCommandLine(base::CommandLine* command_line) override { + MixinBasedInProcessBrowserTest::SetUpCommandLine(command_line); + ASSERT_TRUE(embedded_test_server_.InitializeAndListen()); + const GURL base_url = embedded_test_server_.base_url(); + command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec()); + } + + void SetUpOnMainThread() override { + MixinBasedInProcessBrowserTest::SetUpOnMainThread(); + fake_gaia_.Initialize(); + + FakeGaia::MergeSessionParams params; + params.signed_out_gaia_ids.push_back(kTestGaiaId); + fake_gaia_.UpdateMergeSessionParams(params); + + embedded_test_server_.StartAcceptingConnections(); + +#if BUILDFLAG(IS_CHROMEOS_ASH) + // ChromeSigninClient uses chromeos::DelayNetworkCall() which requires + // simulating being online. + network_portal_detector_.SimulateDefaultNetworkState( + ash::NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_ONLINE); +#endif + } + + FakeGaia fake_gaia_; + net::EmbeddedTestServer embedded_test_server_; + +#if BUILDFLAG(IS_CHROMEOS_ASH) + ash::NetworkPortalDetectorMixin network_portal_detector_{&mixin_host_}; +#endif +}; + +IN_PROC_BROWSER_TEST_F(RemoveLocalAccountTest, ShouldNotifyObservers) { + // To enforce an initial ListAccounts fetch and the corresponding notification + // to observers, make the current list as stale. This is done for the purpose + // of documenting assertions on the AccountsInCookieJarInfo passed to + // observers during notification. + signin::SetFreshnessOfAccountsInGaiaCookie(identity_manager(), + /*accounts_are_fresh=*/false); + + ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh); + const signin::AccountsInCookieJarInfo + cookie_jar_info_in_initial_notification = + WaitUntilAccountsInCookieUpdated(); + ASSERT_TRUE(cookie_jar_info_in_initial_notification.accounts_are_fresh); + ASSERT_THAT(cookie_jar_info_in_initial_notification.signed_out_accounts, + Contains(ListedAccountMatchesGaiaId(kTestGaiaId))); + + const signin::AccountsInCookieJarInfo initial_cookie_jar_info = + identity_manager()->GetAccountsInCookieJar(); + ASSERT_TRUE(initial_cookie_jar_info.accounts_are_fresh); + ASSERT_THAT(initial_cookie_jar_info.signed_out_accounts, + Contains(ListedAccountMatchesGaiaId(kTestGaiaId))); + + // Open a FakeGaia page that issues the desired HTTP response header with + // Google-Accounts-RemoveLocalAccount. + chrome::AddTabAt(browser(), + fake_gaia_.GetDummyRemoveLocalAccountURL(kTestGaiaId), + /*index=*/0, + /*foreground=*/true); + + // Wait until observers are notified with OnAccountsInCookieUpdated(). + const signin::AccountsInCookieJarInfo + cookie_jar_info_in_updated_notification = + WaitUntilAccountsInCookieUpdated(); + + EXPECT_TRUE(cookie_jar_info_in_updated_notification.accounts_are_fresh); + EXPECT_THAT(cookie_jar_info_in_updated_notification.signed_out_accounts, + Not(Contains(ListedAccountMatchesGaiaId(kTestGaiaId)))); + + const signin::AccountsInCookieJarInfo updated_cookie_jar_info = + identity_manager()->GetAccountsInCookieJar(); + EXPECT_TRUE(updated_cookie_jar_info.accounts_are_fresh); + EXPECT_THAT(updated_cookie_jar_info.signed_out_accounts, + Not(Contains(ListedAccountMatchesGaiaId(kTestGaiaId)))); +} + +} // namespace diff --git a/chromium/chrome/browser/signin/services/DIR_METADATA b/chromium/chrome/browser/signin/services/DIR_METADATA new file mode 100644 index 00000000000..7a2580a646c --- /dev/null +++ b/chromium/chrome/browser/signin/services/DIR_METADATA @@ -0,0 +1 @@ +os: ANDROID diff --git a/chromium/chrome/browser/signin/services/OWNERS b/chromium/chrome/browser/signin/services/OWNERS new file mode 100644 index 00000000000..1c49383244f --- /dev/null +++ b/chromium/chrome/browser/signin/services/OWNERS @@ -0,0 +1,2 @@ +bsazonov@chromium.org +aliceywang@chromium.org diff --git a/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java new file mode 100644 index 00000000000..f64eb974942 --- /dev/null +++ b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/ProfileDataCacheUnitTest.java @@ -0,0 +1,120 @@ +// Copyright 2021 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.signin.services; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.quality.Strictness; +import org.robolectric.RuntimeEnvironment; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.base.test.util.JniMocker; +import org.chromium.chrome.R; +import org.chromium.chrome.test.util.browser.signin.AccountManagerTestRule; +import org.chromium.components.signin.base.AccountInfo; +import org.chromium.components.signin.base.CoreAccountId; +import org.chromium.components.signin.identitymanager.AccountInfoServiceProvider; +import org.chromium.components.signin.identitymanager.AccountTrackerService; +import org.chromium.components.signin.identitymanager.IdentityManager; +import org.chromium.components.signin.identitymanager.IdentityManagerJni; + +/** + * Unit tests for {@link ProfileDataCache} + */ +@RunWith(BaseRobolectricTestRunner.class) +public class ProfileDataCacheUnitTest { + private static final long NATIVE_IDENTITY_MANAGER = 10001L; + private static final String ACCOUNT_EMAIL = "test@gmail.com"; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); + + @Rule + public final JniMocker mocker = new JniMocker(); + + @Rule + public final AccountManagerTestRule mAccountManagerTestRule = new AccountManagerTestRule(); + + @Mock + private AccountTrackerService mAccountTrackerServiceMock; + + @Mock + private IdentityManager.Natives mIdentityManagerNativeMock; + + @Mock + private ProfileDataCache.Observer mObserverMock; + + private final IdentityManager mIdentityManager = + IdentityManager.create(NATIVE_IDENTITY_MANAGER, null /* OAuth2TokenService */); + + private ProfileDataCache mProfileDataCache; + + @Before + public void setUp() { + mocker.mock(IdentityManagerJni.TEST_HOOKS, mIdentityManagerNativeMock); + mProfileDataCache = ProfileDataCache.createWithDefaultImageSizeAndNoBadge( + RuntimeEnvironment.application.getApplicationContext()); + + // Add an observer for IdentityManager::onExtendedAccountInfoUpdated. + mAccountManagerTestRule.observeIdentityManager(mIdentityManager); + } + + @After + public void tearDown() { + AccountInfoServiceProvider.resetForTests(); + } + + @Test + public void accountInfoIsUpdatedWithOnlyFullName() { + final String fullName = "full name1"; + final AccountInfo accountInfo = new AccountInfo(new CoreAccountId("gaia-id-test"), + ACCOUNT_EMAIL, "gaia-id-test", fullName, null, null); + mProfileDataCache.addObserver(mObserverMock); + Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + Assert.assertNull(mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getFullName()); + + mIdentityManager.onExtendedAccountInfoUpdated(accountInfo); + + Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + Assert.assertEquals( + fullName, mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getFullName()); + } + + @Test + public void accountInfoIsUpdatedWithOnlyGivenName() { + final String givenName = "given name1"; + final AccountInfo accountInfo = new AccountInfo(new CoreAccountId("gaia-id-test"), + ACCOUNT_EMAIL, "gaia-id-test", null, givenName, null); + mProfileDataCache.addObserver(mObserverMock); + Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + Assert.assertNull(mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getGivenName()); + + mIdentityManager.onExtendedAccountInfoUpdated(accountInfo); + + Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + Assert.assertEquals( + givenName, mProfileDataCache.getProfileDataOrDefault(ACCOUNT_EMAIL).getGivenName()); + } + + @Test + public void accountInfoIsUpdatedWithOnlyBadgeConfig() { + mProfileDataCache.setBadge(R.drawable.ic_sync_badge_error_20dp); + final AccountInfo accountInfo = new AccountInfo( + new CoreAccountId("gaia-id-test"), ACCOUNT_EMAIL, "gaia-id-test", null, null, null); + mProfileDataCache.addObserver(mObserverMock); + Assert.assertFalse(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + + mIdentityManager.onExtendedAccountInfoUpdated(accountInfo); + + Assert.assertTrue(mProfileDataCache.hasProfileData(ACCOUNT_EMAIL)); + } +} diff --git a/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java new file mode 100644 index 00000000000..8acec65e1e9 --- /dev/null +++ b/chromium/chrome/browser/signin/services/android/junit/src/org/chromium/chrome/browser/signin/services/WebSigninBridgeTest.java @@ -0,0 +1,89 @@ +// 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.signin.services; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.base.test.util.JniMocker; +import org.chromium.chrome.browser.profiles.Profile; +import org.chromium.components.signin.base.CoreAccountInfo; +import org.chromium.components.signin.base.GoogleServiceAuthError; +import org.chromium.components.signin.base.GoogleServiceAuthError.State; + +/** + * Unit tests for {@link WebSigninBridge}. + */ +@RunWith(BaseRobolectricTestRunner.class) +public class WebSigninBridgeTest { + private static final CoreAccountInfo CORE_ACCOUNT_INFO = + CoreAccountInfo.createFromEmailAndGaiaId("user@domain.com", "gaia-id-user"); + private static final long NATIVE_WEB_SIGNIN_BRIDGE = 1000L; + + @Rule + public final JniMocker mocker = new JniMocker(); + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock + private WebSigninBridge.Natives mNativeMock; + + @Mock + private Profile mProfileMock; + + @Mock + private WebSigninBridge.Listener mListenerMock; + + private final WebSigninBridge.Factory mFactory = new WebSigninBridge.Factory(); + + @Before + public void setUp() { + mocker.mock(WebSigninBridgeJni.TEST_HOOKS, mNativeMock); + when(mNativeMock.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock)) + .thenReturn(NATIVE_WEB_SIGNIN_BRIDGE); + } + + @Test + public void testFactoryCreate() { + WebSigninBridge webSigninBridge = + mFactory.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock); + Assert.assertNotNull("Factory#create should not return null!", webSigninBridge); + verify(mNativeMock).create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock); + } + + @Test + public void testDestroy() { + mFactory.create(mProfileMock, CORE_ACCOUNT_INFO, mListenerMock).destroy(); + verify(mNativeMock).destroy(NATIVE_WEB_SIGNIN_BRIDGE); + } + + @Test + public void testOnSigninSucceed() { + WebSigninBridge.onSigninSucceeded(mListenerMock); + verify(mListenerMock).onSigninSucceeded(); + verify(mListenerMock, never()).onSigninFailed(any()); + } + + @Test + public void testOnSigninFailed() { + final GoogleServiceAuthError error = new GoogleServiceAuthError(State.CONNECTION_FAILED); + WebSigninBridge.onSigninFailed(mListenerMock, error); + verify(mListenerMock).onSigninFailed(error); + verify(mListenerMock, never()).onSigninSucceeded(); + } +} diff --git a/chromium/chrome/browser/signin/signin_error_controller_factory.cc b/chromium/chrome/browser/signin/signin_error_controller_factory.cc new file mode 100644 index 00000000000..ebbd0612ba9 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_error_controller_factory.cc @@ -0,0 +1,48 @@ +// 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. + +#include "chrome/browser/signin/signin_error_controller_factory.h" + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +SigninErrorControllerFactory::SigninErrorControllerFactory() + : BrowserContextKeyedServiceFactory( + "SigninErrorController", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +SigninErrorControllerFactory::~SigninErrorControllerFactory() {} + +// static +SigninErrorController* SigninErrorControllerFactory::GetForProfile( + Profile* profile) { + return static_cast<SigninErrorController*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +SigninErrorControllerFactory* SigninErrorControllerFactory::GetInstance() { + return base::Singleton<SigninErrorControllerFactory>::get(); +} + +KeyedService* SigninErrorControllerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + SigninErrorController::AccountMode account_mode = +#if BUILDFLAG(IS_CHROMEOS_ASH) + SigninErrorController::AccountMode::ANY_ACCOUNT; +#else + AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile) + ? SigninErrorController::AccountMode::ANY_ACCOUNT + : SigninErrorController::AccountMode::PRIMARY_ACCOUNT; +#endif + return new SigninErrorController( + account_mode, IdentityManagerFactory::GetForProfile(profile)); +} diff --git a/chromium/chrome/browser/signin/signin_error_controller_factory.h b/chromium/chrome/browser/signin/signin_error_controller_factory.h new file mode 100644 index 00000000000..0d87f49799e --- /dev/null +++ b/chromium/chrome/browser/signin/signin_error_controller_factory.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "components/signin/core/browser/signin_error_controller.h" + +class Profile; + +// Singleton that owns all SigninErrorControllers and associates them with +// Profiles. +class SigninErrorControllerFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of SigninErrorController associated with this profile + // (creating one if none exists). Returns NULL if this profile cannot have an + // SigninClient (for example, if |profile| is incognito). + static SigninErrorController* GetForProfile(Profile* profile); + + // Returns an instance of the factory singleton. + static SigninErrorControllerFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits<SigninErrorControllerFactory>; + + SigninErrorControllerFactory(); + ~SigninErrorControllerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_CONTROLLER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_features.cc b/chromium/chrome/browser/signin/signin_features.cc new file mode 100644 index 00000000000..b1932809890 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_features.cc @@ -0,0 +1,16 @@ +// 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. + +#include "chrome/browser/signin/signin_features.h" + +// Enables the client-side processing of the HTTP response header +// Google-Accounts-RemoveLocalAccount. +const base::Feature kProcessGaiaRemoveLocalAccountHeader{ + "ProcessGaiaRemoveLocalAccountHeader", base::FEATURE_ENABLED_BY_DEFAULT}; + +// Allows policies to be loaded on a managed account without activating sync. +// Uses enterprise confirmation dialog for managed accounts signin outside of +// the profile picker. +const base::Feature kAccountPoliciesLoadedWithoutSync{ + "AccountPoliciesLoadedWithoutSync", base::FEATURE_DISABLED_BY_DEFAULT}; diff --git a/chromium/chrome/browser/signin/signin_features.h b/chromium/chrome/browser/signin/signin_features.h new file mode 100644 index 00000000000..47c61c21949 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_features.h @@ -0,0 +1,15 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_ + +#include "base/feature_list.h" +#include "build/chromeos_buildflags.h" + +extern const base::Feature kProcessGaiaRemoveLocalAccountHeader; + +extern const base::Feature kAccountPoliciesLoadedWithoutSync; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_FEATURES_H_ diff --git a/chromium/chrome/browser/signin/signin_global_error.cc b/chromium/chrome/browser/signin/signin_global_error.cc new file mode 100644 index 00000000000..d02d7ad0158 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error.cc @@ -0,0 +1,170 @@ +// Copyright (c) 2013 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. + +#include "chrome/browser/signin/signin_global_error.h" + +#include "base/logging.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/browser/ui/chrome_pages.h" +#include "chrome/browser/ui/global_error/global_error_service.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "chrome/browser/ui/singleton_tabs.h" +#include "chrome/browser/ui/webui/signin/login_ui_service.h" +#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h" +#include "chrome/common/url_constants.h" +#include "chrome/grit/chromium_strings.h" +#include "chrome/grit/generated_resources.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "net/base/url_util.h" +#include "ui/base/l10n/l10n_util.h" + +#if !defined(OS_ANDROID) +#include "chrome/browser/signin/signin_promo.h" +#endif + +SigninGlobalError::SigninGlobalError( + SigninErrorController* error_controller, + Profile* profile) + : profile_(profile), + error_controller_(error_controller) { + error_controller_->AddObserver(this); +} + +SigninGlobalError::~SigninGlobalError() { + DCHECK(!error_controller_) + << "SigninGlobalError::Shutdown() was not called"; +} + +bool SigninGlobalError::HasError() { + return HasMenuItem(); +} + +void SigninGlobalError::Shutdown() { + error_controller_->RemoveObserver(this); + error_controller_ = nullptr; +} + +bool SigninGlobalError::HasMenuItem() { + return error_controller_->HasError(); +} + +int SigninGlobalError::MenuItemCommandID() { + return IDC_SHOW_SIGNIN_ERROR; +} + +std::u16string SigninGlobalError::MenuItemLabel() { + // Notify the user if there's an auth error the user should know about. + if (error_controller_->HasError()) + return l10n_util::GetStringUTF16(IDS_SYNC_SIGN_IN_ERROR_WRENCH_MENU_ITEM); + return std::u16string(); +} + +void SigninGlobalError::ExecuteMenuItem(Browser* browser) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + if (error_controller_->auth_error().state() != + GoogleServiceAuthError::NONE) { + DVLOG(1) << "Signing out the user to fix a sync error."; + // TODO(beng): seems like this could just call chrome::AttemptUserExit(). + chrome::ExecuteCommand(browser, IDC_EXIT); + return; + } +#endif + + // Global errors don't show up in the wrench menu on mobile. +#if !defined(OS_ANDROID) + LoginUIService* login_ui = LoginUIServiceFactory::GetForProfile(profile_); + if (login_ui->current_login_ui()) { + login_ui->current_login_ui()->FocusUI(); + return; + } + + browser->window()->ShowAvatarBubbleFromAvatarButton( + BrowserWindow::AVATAR_BUBBLE_MODE_REAUTH, + signin_metrics::AccessPoint::ACCESS_POINT_MENU, false); +#endif +} + +bool SigninGlobalError::HasBubbleView() { + return !GetBubbleViewMessages().empty(); +} + +std::u16string SigninGlobalError::GetBubbleViewTitle() { + return l10n_util::GetStringUTF16(IDS_SIGNIN_ERROR_BUBBLE_VIEW_TITLE); +} + +std::vector<std::u16string> SigninGlobalError::GetBubbleViewMessages() { + std::vector<std::u16string> messages; + + // If the user isn't signed in, no need to display an error bubble. + auto* identity_manager = + IdentityManagerFactory::GetForProfileIfExists(profile_); + if (identity_manager && + !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + return messages; + } + + if (!error_controller_->HasError()) + return messages; + + switch (error_controller_->auth_error().state()) { + // TODO(rogerta): use account id in error messages. + + // User credentials are invalid (bad acct, etc). + case GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS: + case GoogleServiceAuthError::SERVICE_ERROR: + messages.push_back(l10n_util::GetStringUTF16( + IDS_SYNC_SIGN_IN_ERROR_BUBBLE_VIEW_MESSAGE)); + break; + + // Sync service is not available for this account's domain. + case GoogleServiceAuthError::SERVICE_UNAVAILABLE: + messages.push_back(l10n_util::GetStringUTF16( + IDS_SYNC_UNAVAILABLE_ERROR_BUBBLE_VIEW_MESSAGE)); + break; + + // Generic message for "other" errors. + default: + messages.push_back(l10n_util::GetStringUTF16( + IDS_SYNC_OTHER_SIGN_IN_ERROR_BUBBLE_VIEW_MESSAGE)); + } + return messages; +} + +std::u16string SigninGlobalError::GetBubbleViewAcceptButtonLabel() { + // If the auth service is unavailable, don't give the user the option to try + // signing in again. + if (error_controller_->auth_error().state() == + GoogleServiceAuthError::SERVICE_UNAVAILABLE) { + return l10n_util::GetStringUTF16( + IDS_SYNC_UNAVAILABLE_ERROR_BUBBLE_VIEW_ACCEPT); + } else { + return l10n_util::GetStringUTF16(IDS_SYNC_SIGN_IN_ERROR_BUBBLE_VIEW_ACCEPT); + } +} + +std::u16string SigninGlobalError::GetBubbleViewCancelButtonLabel() { + return std::u16string(); +} + +void SigninGlobalError::OnBubbleViewDidClose(Browser* browser) { +} + +void SigninGlobalError::BubbleViewAcceptButtonPressed(Browser* browser) { + ExecuteMenuItem(browser); +} + +void SigninGlobalError::BubbleViewCancelButtonPressed(Browser* browser) { + NOTREACHED(); +} + +void SigninGlobalError::OnErrorChanged() { + GlobalErrorServiceFactory::GetForProfile(profile_)->NotifyErrorsChanged(); +} diff --git a/chromium/chrome/browser/signin/signin_global_error.h b/chromium/chrome/browser/signin/signin_global_error.h new file mode 100644 index 00000000000..50e110ea0bc --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error.h @@ -0,0 +1,64 @@ +// Copyright (c) 2013 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_ + +#include <set> +#include "base/gtest_prod_util.h" +#include "base/memory/raw_ptr.h" +#include "chrome/browser/ui/global_error/global_error.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/signin/core/browser/signin_error_controller.h" + +class Profile; + +// Shows auth errors on the wrench menu using a bubble view and a menu item. +class SigninGlobalError : public GlobalErrorWithStandardBubble, + public SigninErrorController::Observer, + public KeyedService { + public: + SigninGlobalError(SigninErrorController* error_controller, + Profile* profile); + + SigninGlobalError(const SigninGlobalError&) = delete; + SigninGlobalError& operator=(const SigninGlobalError&) = delete; + + ~SigninGlobalError() override; + + // Returns true if there is an authentication error. + bool HasError(); + + private: + FRIEND_TEST_ALL_PREFIXES(SigninGlobalErrorTest, Basic); + FRIEND_TEST_ALL_PREFIXES(SigninGlobalErrorTest, AuthStatusEnumerateAllErrors); + + // KeyedService: + void Shutdown() override; + + // GlobalErrorWithStandardBubble: + bool HasMenuItem() override; + int MenuItemCommandID() override; + std::u16string MenuItemLabel() override; + void ExecuteMenuItem(Browser* browser) override; + bool HasBubbleView() override; + std::u16string GetBubbleViewTitle() override; + std::vector<std::u16string> GetBubbleViewMessages() override; + std::u16string GetBubbleViewAcceptButtonLabel() override; + std::u16string GetBubbleViewCancelButtonLabel() override; + void OnBubbleViewDidClose(Browser* browser) override; + void BubbleViewAcceptButtonPressed(Browser* browser) override; + void BubbleViewCancelButtonPressed(Browser* browser) override; + + // SigninErrorController::Observer: + void OnErrorChanged() override; + + // The Profile this service belongs to. + raw_ptr<Profile> profile_; + + // The SigninErrorController that provides auth status. + raw_ptr<SigninErrorController> error_controller_; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_H_ diff --git a/chromium/chrome/browser/signin/signin_global_error_factory.cc b/chromium/chrome/browser/signin/signin_global_error_factory.cc new file mode 100644 index 00000000000..304d6a59cce --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error_factory.cc @@ -0,0 +1,46 @@ +// Copyright 2014 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. + +#include "chrome/browser/signin/signin_global_error_factory.h" + +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/signin_error_controller_factory.h" +#include "chrome/browser/signin/signin_global_error.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +SigninGlobalErrorFactory::SigninGlobalErrorFactory() + : BrowserContextKeyedServiceFactory( + "SigninGlobalError", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(SigninErrorControllerFactory::GetInstance()); + DependsOn(GlobalErrorServiceFactory::GetInstance()); +} + +SigninGlobalErrorFactory::~SigninGlobalErrorFactory() {} + +// static +SigninGlobalError* SigninGlobalErrorFactory::GetForProfile( + Profile* profile) { + return static_cast<SigninGlobalError*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +SigninGlobalErrorFactory* SigninGlobalErrorFactory::GetInstance() { + return base::Singleton<SigninGlobalErrorFactory>::get(); +} + +KeyedService* SigninGlobalErrorFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { +#if BUILDFLAG(IS_CHROMEOS_ASH) + return nullptr; +#endif + + Profile* profile = static_cast<Profile*>(context); + return new SigninGlobalError( + SigninErrorControllerFactory::GetForProfile(profile), profile); +} diff --git a/chromium/chrome/browser/signin/signin_global_error_factory.h b/chromium/chrome/browser/signin/signin_global_error_factory.h new file mode 100644 index 00000000000..d13b4f6df61 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error_factory.h @@ -0,0 +1,40 @@ +// Copyright 2014 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class SigninGlobalError; +class Profile; + +// Singleton that owns all SigninGlobalErrors and associates them with +// Profiles. Listens for the Profile's destruction notification and cleans up +// the associated SigninGlobalError. +class SigninGlobalErrorFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns the instance of SigninGlobalError associated with this + // profile, creating one if none exists. In Ash, this will return NULL. + static SigninGlobalError* GetForProfile(Profile* profile); + + // Returns an instance of the SigninGlobalErrorFactory singleton. + static SigninGlobalErrorFactory* GetInstance(); + + SigninGlobalErrorFactory(const SigninGlobalErrorFactory&) = delete; + SigninGlobalErrorFactory& operator=(const SigninGlobalErrorFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits<SigninGlobalErrorFactory>; + + SigninGlobalErrorFactory(); + ~SigninGlobalErrorFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_GLOBAL_ERROR_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_global_error_unittest.cc b/chromium/chrome/browser/signin/signin_global_error_unittest.cc new file mode 100644 index 00000000000..8d4f5f66eb6 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_global_error_unittest.cc @@ -0,0 +1,166 @@ +// Copyright (c) 2013 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. + +#include "chrome/browser/signin/signin_global_error.h" + +#include <stddef.h> + +#include <memory> +#include <string> + +#include "base/bind.h" +#include "base/cxx17_backports.h" +#include "base/memory/raw_ptr.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/metrics/histogram_tester.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_metrics.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/signin/signin_error_controller_factory.h" +#include "chrome/browser/signin/signin_global_error_factory.h" +#include "chrome/browser/ui/global_error/global_error_service.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/signin_error_controller.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/sync_preferences/pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +static const char kTestEmail[] = "testuser@test.com"; +static const char16_t kTestEmail16[] = u"testuser@test.com"; + +class SigninGlobalErrorTest : public testing::Test { + public: + SigninGlobalErrorTest() : + profile_manager_(TestingBrowserProcess::GetGlobal()) {} + + void SetUp() override { + ASSERT_TRUE(profile_manager_.SetUp()); + + // Create a signed-in profile. + TestingProfile::TestingFactories testing_factories = + IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories(); + + profile_ = profile_manager_.CreateTestingProfile( + "Person 1", std::unique_ptr<sync_preferences::PrefServiceSyncable>(), + u"Person 1", 0, std::string(), std::move(testing_factories)); + + identity_test_env_profile_adaptor_ = + std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile()); + + AccountInfo account_info = + identity_test_env_profile_adaptor_->identity_test_env() + ->MakePrimaryAccountAvailable(kTestEmail, + signin::ConsentLevel::kSync); + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile()->GetPath()); + ASSERT_NE(entry, nullptr); + + entry->SetAuthInfo(account_info.gaia, kTestEmail16, + /*is_consented_primary_account=*/true); + + global_error_ = SigninGlobalErrorFactory::GetForProfile(profile()); + error_controller_ = SigninErrorControllerFactory::GetForProfile(profile()); + } + + TestingProfile* profile() { return profile_; } + TestingProfileManager* testing_profile_manager() { + return &profile_manager_; + } + + SigninGlobalError* global_error() { return global_error_; } + SigninErrorController* error_controller() { return error_controller_; } + + void SetAuthError(GoogleServiceAuthError::State state) { + signin::IdentityTestEnvironment* identity_test_env = + identity_test_env_profile_adaptor_->identity_test_env(); + CoreAccountId primary_account_id = + identity_test_env->identity_manager()->GetPrimaryAccountId( + signin::ConsentLevel::kSync); + + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + identity_test_env->identity_manager(), primary_account_id, + GoogleServiceAuthError(state)); + } + + private: + content::BrowserTaskEnvironment task_environment_; + TestingProfileManager profile_manager_; + raw_ptr<TestingProfile> profile_; + + std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> + identity_test_env_profile_adaptor_; + + raw_ptr<SigninGlobalError> global_error_; + raw_ptr<SigninErrorController> error_controller_; +}; + +TEST_F(SigninGlobalErrorTest, Basic) { + ASSERT_FALSE(global_error()->HasMenuItem()); + + SetAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + EXPECT_TRUE(global_error()->HasMenuItem()); + + SetAuthError(GoogleServiceAuthError::NONE); + EXPECT_FALSE(global_error()->HasMenuItem()); +} + +// Verify that SigninGlobalError ignores certain errors. +TEST_F(SigninGlobalErrorTest, AuthStatusEnumerateAllErrors) { + typedef struct { + GoogleServiceAuthError::State error_state; + bool is_error; + } ErrorTableEntry; + + ErrorTableEntry table[] = { + {GoogleServiceAuthError::NONE, false}, + {GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, true}, + {GoogleServiceAuthError::USER_NOT_SIGNED_UP, true}, + {GoogleServiceAuthError::CONNECTION_FAILED, false}, + {GoogleServiceAuthError::SERVICE_UNAVAILABLE, false}, + {GoogleServiceAuthError::REQUEST_CANCELED, false}, + {GoogleServiceAuthError::UNEXPECTED_SERVICE_RESPONSE, true}, + {GoogleServiceAuthError::SERVICE_ERROR, true}, + }; + static_assert( + base::size(table) == GoogleServiceAuthError::NUM_STATES - + GoogleServiceAuthError::kDeprecatedStateCount, + "table size should match number of auth error types"); + + // Mark the profile with an active timestamp so profile_metrics logs it. + testing_profile_manager()->UpdateLastUser(profile()); + + for (ErrorTableEntry entry : table) { + SetAuthError(GoogleServiceAuthError::NONE); + + base::HistogramTester histogram_tester; + SetAuthError(entry.error_state); + + EXPECT_EQ(global_error()->HasMenuItem(), entry.is_error); + EXPECT_EQ(global_error()->MenuItemLabel().empty(), !entry.is_error); + EXPECT_EQ(global_error()->GetBubbleViewMessages().empty(), !entry.is_error); + EXPECT_FALSE(global_error()->GetBubbleViewTitle().empty()); + EXPECT_FALSE(global_error()->GetBubbleViewAcceptButtonLabel().empty()); + EXPECT_TRUE(global_error()->GetBubbleViewCancelButtonLabel().empty()); + + ProfileMetrics::LogNumberOfProfiles(&testing_profile_manager() + ->profile_manager() + ->GetProfileAttributesStorage()); + + if (entry.is_error) { + histogram_tester.ExpectBucketCount("Signin.AuthError", entry.error_state, + 1); + } + } +} diff --git a/chromium/chrome/browser/signin/signin_manager.cc b/chromium/chrome/browser/signin/signin_manager.cc new file mode 100644 index 00000000000..9cc73e0e49e --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager.cc @@ -0,0 +1,200 @@ +// 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. + +#include "chrome/browser/signin/signin_manager.h" + +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" + +SigninManager::SigninManager(PrefService* prefs, + signin::IdentityManager* identity_manager) + : prefs_(prefs), identity_manager_(identity_manager) { + signin_allowed_.Init( + prefs::kSigninAllowed, prefs_, + base::BindRepeating(&SigninManager::OnSigninAllowedPrefChanged, + base::Unretained(this))); + + UpdateUnconsentedPrimaryAccount(); + identity_manager_->AddObserver(this); +} + +SigninManager::~SigninManager() { + identity_manager_->RemoveObserver(this); +} + +void SigninManager::UpdateUnconsentedPrimaryAccount() { + // Only update the unconsented primary account only after accounts are loaded. + if (!identity_manager_->AreRefreshTokensLoaded()) { + return; + } + + absl::optional<CoreAccountInfo> account = + ComputeUnconsentedPrimaryAccountInfo(); + + DCHECK(!account || !account->IsEmpty()); + if (account) { + if (identity_manager_->GetPrimaryAccountInfo( + signin::ConsentLevel::kSignin) != account) { + DCHECK( + !identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)); + identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount( + account->account_id, signin::ConsentLevel::kSignin); + } + } else if (identity_manager_->HasPrimaryAccount( + signin::ConsentLevel::kSignin)) { + DCHECK(!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)); + identity_manager_->GetPrimaryAccountMutator()->ClearPrimaryAccount( + signin_metrics::USER_DELETED_ACCOUNT_COOKIES, + signin_metrics::SignoutDelete::kIgnoreMetric); + } +} + +absl::optional<CoreAccountInfo> +SigninManager::ComputeUnconsentedPrimaryAccountInfo() const { + DCHECK(identity_manager_->AreRefreshTokensLoaded()); + + // UPA is equal to the primary account with sync consent if it exists. + if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + return identity_manager_->GetPrimaryAccountInfo( + signin::ConsentLevel::kSync); + } + + // Clearing the primary sync account when sign-in is not allowed is handled + // by PrimaryAccountPolicyManager. That flow is extremely hard to follow + // especially for the case when the user is syncing with a managed account + // as in that case the whole profile needs to be deleted. + // + // It was considered simpler to keep the logic to update the unconsented + // primary account in a single place. + if (!signin_allowed_.GetValue()) + return absl::nullopt; + + signin::AccountsInCookieJarInfo cookie_info = + identity_manager_->GetAccountsInCookieJar(); + + std::vector<gaia::ListedAccount> cookie_accounts = + cookie_info.signed_in_accounts; + + // Fresh cookies and loaded tokens are needed to compute the UPA. + if (cookie_info.accounts_are_fresh) { + // Cookies are fresh and tokens are loaded, UPA is the first account + // in cookies if it exists and has a refresh token. + if (cookie_accounts.empty()) { + // Cookies are empty, the UPA is empty. + return absl::nullopt; + } + + AccountInfo account_info = + identity_manager_->FindExtendedAccountInfoByAccountId( + cookie_accounts[0].id); + + // Verify the first account in cookies has a refresh token that is valid. + bool error_state = + account_info.IsEmpty() || + identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState( + account_info.account_id); + + return error_state ? absl::nullopt + : absl::make_optional<CoreAccountInfo>(account_info); + } + + if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) + return absl::nullopt; + + // If cookies or tokens are not loaded, it is not possible to fully compute + // the unconsented primary account. However, if the current unconsented + // primary account is no longer valid, it has to be removed. + CoreAccountId current_account = + identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSignin); + + if (!identity_manager_->HasAccountWithRefreshToken(current_account)) { + // Tokens are loaded, but the current UPA doesn't have a refresh token. + // Clear the current UPA. + return absl::nullopt; + } + + if (cookie_info.accounts_are_fresh) { + if (cookie_accounts.empty() || cookie_accounts[0].id != current_account) { + // The current UPA is not the first in fresh cookies. It needs to be + // cleared. + return absl::nullopt; + } + } + + // No indication that the current UPA is invalid, return current UPA. + return identity_manager_->GetPrimaryAccountInfo( + signin::ConsentLevel::kSignin); +} + +// signin::IdentityManager::Observer implementation. +void SigninManager::OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event_details) { + // This is needed for the case where the user chooses to start syncing + // with an account that is different from the unconsented primary account + // (not the first in cookies) but then cancels. In that case, the tokens stay + // the same. In all the other cases, either the token will be revoked which + // will trigger an update for the unconsented primary account or the + // primary account stays the same but the sync consent is revoked. + if (event_details.GetEventTypeFor(signin::ConsentLevel::kSync) != + signin::PrimaryAccountChangeEvent::Type::kCleared) { + return; + } + + // It is important to update the primary account after all observers process + // the current OnPrimaryAccountChanged() as all observers should see the same + // value for the unconsented primary account. Schedule the potential update + // on the next run loop. + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&SigninManager::UpdateUnconsentedPrimaryAccount, + weak_ptr_factory_.GetWeakPtr())); +} + +void SigninManager::OnRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info) { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnRefreshTokenRemovedForAccount( + const CoreAccountId& account_id) { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnRefreshTokensLoaded() { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnAccountsInCookieUpdated( + const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, + const GoogleServiceAuthError& error) { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnAccountsCookieDeletedByUserAction() { + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnErrorStateOfRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info, + const GoogleServiceAuthError& error) { + CoreAccountInfo current_account = + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + + bool should_update = false; + if (error == GoogleServiceAuthError::AuthErrorNone()) { + should_update = current_account.IsEmpty(); + } else { + // In error state, update if the account in error is the current UPA. + should_update = (account_info == current_account); + } + + if (should_update) + UpdateUnconsentedPrimaryAccount(); +} + +void SigninManager::OnSigninAllowedPrefChanged() { + UpdateUnconsentedPrimaryAccount(); +} diff --git a/chromium/chrome/browser/signin/signin_manager.h b/chromium/chrome/browser/signin/signin_manager.h new file mode 100644 index 00000000000..2feaf8dc737 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager.h @@ -0,0 +1,71 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_ + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/prefs/pref_member.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +class PrefService; + +class SigninManager : public KeyedService, + public signin::IdentityManager::Observer { + public: + SigninManager(PrefService* prefs, signin::IdentityManager* identity_manger); + SigninManager(const SigninManager&) = delete; + SigninManager& operator=(const SigninManager&) = delete; + + ~SigninManager() override; + + private: + // Updates the cached version of unconsented primary account and notifies the + // observers if there is any change. + void UpdateUnconsentedPrimaryAccount(); + + // Computes and returns the unconsented primary account (UPA). + // - If a primary account with sync consent exists, the UPA is equal to it. + // - The UPA is the first account in cookies and must have a refresh token. + // For the UPA to be computed, it needs fresh cookies and tokens to be loaded. + // - If tokens are not loaded or cookies are not fresh, the UPA can't be + // computed but if one already exists it might be invalid. That can happen if + // cookies are fresh but are empty or the first account is different than the + // current UPA, the other cases are if tokens are not loaded but the current + // UPA's refresh token has been rekoved or tokens are loaded but the current + // UPA does not have a refresh token. If the UPA is invalid, it needs to be + // cleared, |absl::nullopt| is returned. If it is still valid, returns the + // valid UPA. + absl::optional<CoreAccountInfo> ComputeUnconsentedPrimaryAccountInfo() const; + + // signin::IdentityManager::Observer implementation. + void OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event_details) override; + void OnRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info) override; + void OnRefreshTokenRemovedForAccount( + const CoreAccountId& account_id) override; + void OnRefreshTokensLoaded() override; + void OnAccountsInCookieUpdated( + const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, + const GoogleServiceAuthError& error) override; + void OnAccountsCookieDeletedByUserAction() override; + void OnErrorStateOfRefreshTokenUpdatedForAccount( + const CoreAccountInfo& account_info, + const GoogleServiceAuthError& error) override; + + void OnSigninAllowedPrefChanged(); + + raw_ptr<PrefService> prefs_; + raw_ptr<signin::IdentityManager> identity_manager_; + + // Helper object to listen for changes to the signin allowed preference. + BooleanPrefMember signin_allowed_; + + base::WeakPtrFactory<SigninManager> weak_ptr_factory_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_H_ diff --git a/chromium/chrome/browser/signin/signin_manager_android_factory.cc b/chromium/chrome/browser/signin/signin_manager_android_factory.cc new file mode 100644 index 00000000000..08d545c346a --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_android_factory.cc @@ -0,0 +1,41 @@ +// 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. + +#include "chrome/browser/signin/signin_manager_android_factory.h" + +#include "chrome/browser/android/signin/signin_manager_android.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +SigninManagerAndroidFactory::SigninManagerAndroidFactory() + : BrowserContextKeyedServiceFactory( + "SigninManagerAndroid", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +SigninManagerAndroidFactory::~SigninManagerAndroidFactory() {} + +// static +base::android::ScopedJavaLocalRef<jobject> +SigninManagerAndroidFactory::GetJavaObjectForProfile(Profile* profile) { + return static_cast<SigninManagerAndroid*>( + GetInstance()->GetServiceForBrowserContext(profile, true)) + ->GetJavaObject(); +} + +// static +SigninManagerAndroidFactory* SigninManagerAndroidFactory::GetInstance() { + static base::NoDestructor<SigninManagerAndroidFactory> instance; + return instance.get(); +} + +KeyedService* SigninManagerAndroidFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + + return new SigninManagerAndroid(profile, identity_manager); +} diff --git a/chromium/chrome/browser/signin/signin_manager_android_factory.h b/chromium/chrome/browser/signin/signin_manager_android_factory.h new file mode 100644 index 00000000000..5eabc40186a --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_android_factory.h @@ -0,0 +1,32 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_ + +#include "base/android/scoped_java_ref.h" +#include "base/no_destructor.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class Profile; + +class SigninManagerAndroidFactory : public BrowserContextKeyedServiceFactory { + public: + static base::android::ScopedJavaLocalRef<jobject> GetJavaObjectForProfile( + Profile* profile); + + // Returns an instance of the SigninManagerAndroidFactory singleton. + static SigninManagerAndroidFactory* GetInstance(); + + private: + friend class base::NoDestructor<SigninManagerAndroidFactory>; + SigninManagerAndroidFactory(); + + ~SigninManagerAndroidFactory() override; + + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_ANDROID_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_manager_factory.cc b/chromium/chrome/browser/signin/signin_manager_factory.cc new file mode 100644 index 00000000000..788dbee7f54 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_factory.cc @@ -0,0 +1,48 @@ +// 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. + +#include "chrome/browser/signin/signin_manager_factory.h" + +#include "base/logging.h" +#include "build/build_config.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +// static +SigninManagerFactory* SigninManagerFactory::GetInstance() { + return base::Singleton<SigninManagerFactory>::get(); +} + +// static +SigninManager* SigninManagerFactory::GetForProfile(Profile* profile) { + DCHECK(profile); + return static_cast<SigninManager*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +SigninManagerFactory::SigninManagerFactory() + : BrowserContextKeyedServiceFactory( + "SigninManager", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +SigninManagerFactory::~SigninManagerFactory() = default; + +KeyedService* SigninManagerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + return new SigninManager(profile->GetPrefs(), + IdentityManagerFactory::GetForProfile(profile)); +} + +bool SigninManagerFactory::ServiceIsCreatedWithBrowserContext() const { + return true; +} + +bool SigninManagerFactory::ServiceIsNULLWhileTesting() const { + return true; +} diff --git a/chromium/chrome/browser/signin/signin_manager_factory.h b/chromium/chrome/browser/signin/signin_manager_factory.h new file mode 100644 index 00000000000..d0ca640e22d --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_factory.h @@ -0,0 +1,33 @@ +// 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/signin_manager.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class SigninManagerFactory : public BrowserContextKeyedServiceFactory { + public: + // Returns an instance of the factory singleton. + static SigninManagerFactory* GetInstance(); + + static SigninManager* GetForProfile(Profile* profile); + + private: + friend struct base::DefaultSingletonTraits<SigninManagerFactory>; + + SigninManagerFactory(); + ~SigninManagerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + bool ServiceIsCreatedWithBrowserContext() const override; + bool ServiceIsNULLWhileTesting() const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_MANAGER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_manager_unittest.cc b/chromium/chrome/browser/signin/signin_manager_unittest.cc new file mode 100644 index 00000000000..e2bb33af9f0 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_manager_unittest.cc @@ -0,0 +1,421 @@ +// 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. +#include "chrome/browser/signin/signin_manager.h" + +#include "base/memory/raw_ptr.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::Mock; + +namespace signin { +namespace { +const char kTestEmail[] = "me@gmail.com"; +const char kTestEmail2[] = "me2@gmail.com"; + +class FakeIdentityManagerObserver : public IdentityManager::Observer { + public: + explicit FakeIdentityManagerObserver(IdentityManager* identity_manager) + : identity_manager_(identity_manager) {} + ~FakeIdentityManagerObserver() override = default; + + void OnPrimaryAccountChanged( + const PrimaryAccountChangeEvent& event) override { + auto current_state = event.GetCurrentState(); + EXPECT_EQ( + current_state.primary_account, + identity_manager_->GetPrimaryAccountInfo(current_state.consent_level)); + events_.push_back(event); + } + + const std::vector<PrimaryAccountChangeEvent>& events() const { + return events_; + } + + void Reset() { events_.clear(); } + + private: + raw_ptr<IdentityManager> identity_manager_; + std::vector<PrimaryAccountChangeEvent> events_; +}; +} // namespace + +class SigninManagerTest : public testing::Test { + public: + SigninManagerTest() + : identity_test_env_(/*test_url_loader_factory=*/nullptr, + /*pref_service=*/&prefs_, + signin::AccountConsistencyMethod::kDice, + /*test_signin_client=*/nullptr), + observer_(identity_test_env_.identity_manager()) {} + + SigninManagerTest(const SigninManagerTest&) = delete; + SigninManagerTest& operator=(const SigninManagerTest&) = delete; + + void SetUp() override { + testing::Test::SetUp(); + RecreateSigninManager(); + identity_manager()->AddObserver(&observer_); + } + + void TearDown() override { identity_manager()->RemoveObserver(&observer_); } + + void RecreateSigninManager() { + signin_manger_ = + std::make_unique<SigninManager>(&prefs_, identity_manager()); + } + + AccountInfo GetAccountInfo(const std::string& email) { + AccountInfo account_info; + account_info.gaia = GetTestGaiaIdForEmail(email); + account_info.account_id = + identity_manager()->PickAccountIdForAccount(account_info.gaia, email); + account_info.email = email; + return account_info; + } + + void ExpectUnconsentedPrimaryAccountSetEvent( + const CoreAccountInfo& expected_primary_account) { + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_TRUE(event.GetPreviousState().primary_account.IsEmpty()); + EXPECT_EQ(expected_primary_account, + event.GetCurrentState().primary_account); + observer().Reset(); + } + + void ExpectUnconsentedPrimaryAccountClearedEvent( + const CoreAccountInfo& expected_cleared_account) { + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(expected_cleared_account, + event.GetPreviousState().primary_account); + EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty()); + observer().Reset(); + } + + void ExpectSyncPrimaryAccountSetEvent( + const CoreAccountInfo& expected_primary_account) { + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_TRUE(event.GetPreviousState().primary_account.IsEmpty()); + EXPECT_EQ(expected_primary_account, + event.GetCurrentState().primary_account); + observer().Reset(); + } + + IdentityManager* identity_manager() { + return identity_test_env_.identity_manager(); + } + + IdentityTestEnvironment* identity_test_env() { return &identity_test_env_; } + + AccountInfo MakeAccountAvailableWithCookies(const std::string& email) { + AccountInfo account = GetAccountInfo(kTestEmail); + identity_test_env_.MakeAccountAvailableWithCookies(account.email, + account.gaia); + EXPECT_FALSE(account.IsEmpty()); + EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + return account; + } + + AccountInfo MakeSyncAccountAvailableWithCookies(const std::string& email) { + AccountInfo account = identity_test_env_.MakePrimaryAccountAvailable( + email, signin::ConsentLevel::kSync); + identity_test_env_.SetCookieAccounts({{account.email, account.gaia}}); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync)); + EXPECT_TRUE(identity_manager()->HasPrimaryAccountWithRefreshToken( + signin::ConsentLevel::kSync)); + return account; + } + + FakeIdentityManagerObserver& observer() { return observer_; } + + sync_preferences::TestingPrefServiceSyncable prefs_; + content::BrowserTaskEnvironment task_environment_; + IdentityTestEnvironment identity_test_env_; + std::unique_ptr<SigninManager> signin_manger_; + FakeIdentityManagerObserver observer_; +}; + +TEST_F( + SigninManagerTest, + UnconsentedPrimaryAccountUpdatedOnItsAccountRefreshTokenUpdateWithValidTokenWhenNoSyncConsent) { + // Add an unconsented primary account, incl. proper cookies. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); +} + +TEST_F( + SigninManagerTest, + UnconsentedPrimaryAccountUpdatedOnItsAccountRefreshTokenUpdateWithInvalidTokenWhenNoSyncConsent) { + // Prerequisite: add an unconsented primary account, incl. proper cookies. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + + // Invalid token. + SetInvalidRefreshTokenForAccount(identity_manager(), account.account_id); + ExpectUnconsentedPrimaryAccountClearedEvent(account); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + + // Update with a valid token. + SetRefreshTokenForAccount(identity_manager(), account.account_id, ""); + ExpectUnconsentedPrimaryAccountSetEvent(account); + EXPECT_EQ(identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin), + account); +} + +TEST_F( + SigninManagerTest, + UnconsentedPrimaryAccountRemovedOnItsAccountRefreshTokenRemovalWhenNoSyncConsent) { + // Prerequisite: Add an unconsented primary account, incl. proper cookies. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + + // With no refresh token, there is no unconsented primary account any more. + identity_test_env()->RemoveRefreshTokenForAccount(account.account_id); + ExpectUnconsentedPrimaryAccountClearedEvent(account); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); +} + +TEST_F(SigninManagerTest, UnconsentedPrimaryAccountNotChangedOnSignout) { + // Set a primary account at sync consent level. + AccountInfo account = MakeSyncAccountAvailableWithCookies(kTestEmail); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync)); + EXPECT_TRUE(identity_manager()->HasPrimaryAccountWithRefreshToken( + signin::ConsentLevel::kSync)); + + // Verify the primary account changed event. + ExpectSyncPrimaryAccountSetEvent(account); + + // Tests that sync primary account is cleared, but unconsented account is not. + identity_test_env()->RevokeSyncConsent(); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(account, event.GetPreviousState().primary_account); + EXPECT_EQ(account, event.GetCurrentState().primary_account); +} + +TEST_F(SigninManagerTest, + UnconsentedPrimaryAccountTokenRevokedWithStaleCookies) { + // Prerequisite: add an unconsented primary account, incl. proper cookies. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + + // Make the cookies stale and remove the account. + // Removing the refresh token for the unconsented primary account is + // sufficient to clear it. + identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(false); + identity_test_env()->RemoveRefreshTokenForAccount(account.account_id); + ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh); + + // Unconsented account was removed. + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountClearedEvent(account); +} + +TEST_F(SigninManagerTest, + UnconsentedPrimaryAccountTokenRevokedWithStaleCookiesMultipleAccounts) { + // Add two accounts with cookies. + AccountInfo main_account = + identity_test_env()->MakeAccountAvailable(kTestEmail); + AccountInfo secondary_account = + identity_test_env()->MakeAccountAvailable(kTestEmail2); + identity_test_env()->SetCookieAccounts( + {{main_account.email, main_account.gaia}, + {secondary_account.email, secondary_account.gaia}}); + + EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + EXPECT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountSetEvent(main_account); + + // Make the cookies stale and remove the main account. + identity_test_env()->SetFreshnessOfAccountsInGaiaCookie(false); + identity_test_env()->RemoveRefreshTokenForAccount(main_account.account_id); + ASSERT_FALSE(identity_manager()->GetAccountsInCookieJar().accounts_are_fresh); + + // Unconsented account was removed. + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountClearedEvent(main_account); +} + +TEST_F(SigninManagerTest, UnconsentedPrimaryAccountDuringLoad) { + // Pre-requisite: Add two accounts with cookies. + AccountInfo main_account = + identity_test_env()->MakeAccountAvailable(kTestEmail); + AccountInfo secondary_account = + identity_test_env()->MakeAccountAvailable(kTestEmail2); + identity_test_env()->SetCookieAccounts( + {{main_account.email, main_account.gaia}, + {secondary_account.email, secondary_account.gaia}}); + ASSERT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + ASSERT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + ExpectUnconsentedPrimaryAccountSetEvent(main_account); + + // Set the token service in "loading" mode. + identity_test_env()->ResetToAccountsNotYetLoadedFromDiskState(); + RecreateSigninManager(); + + // Unconsented primary account is available while tokens are not loaded. + EXPECT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_TRUE(observer().events().empty()); + + // Revoking an unrelated token doesn't change the unconsented primary account. + identity_test_env()->RemoveRefreshTokenForAccount( + secondary_account.account_id); + EXPECT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_TRUE(observer().events().empty()); + + // Revoking the token of the unconsented primary account while the tokens + // are still loading does not change the unconsented primary account. + identity_test_env()->RemoveRefreshTokenForAccount(main_account.account_id); + EXPECT_EQ(main_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + EXPECT_TRUE(observer().events().empty()); + + // Finish the token load should clear the primary account as the token of the + // primary account was revoked. + identity_test_env()->ReloadAccountsFromDisk(); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountClearedEvent(main_account); +} + +TEST_F(SigninManagerTest, + UnconsentedPrimaryAccountUpdatedOnSyncConsentRevoked) { + AccountInfo first_account = + identity_test_env()->MakeAccountAvailable(kTestEmail); + AccountInfo second_account = + identity_test_env()->MakeAccountAvailable(kTestEmail2); + identity_test_env()->SetCookieAccounts( + {{first_account.email, first_account.gaia}, + {second_account.email, second_account.gaia}}); + ASSERT_EQ(first_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + ExpectUnconsentedPrimaryAccountSetEvent(first_account); + + // Set the sync primary account to the second account in cookies. + // The unconsented primary account should be updated. + identity_test_env()->SetPrimaryAccount(second_account.email, + signin::ConsentLevel::kSync); + EXPECT_EQ(second_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSync)); + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(first_account, event.GetPreviousState().primary_account); + EXPECT_EQ(second_account, event.GetCurrentState().primary_account); + observer().Reset(); + + // Clear primary account but do not delete the account. The unconsented + // primary account should be updated to be the first account in cookies. + identity_test_env()->RevokeSyncConsent(); + base::RunLoop().RunUntilIdle(); + + // Primary account is cleared, but unconsented account is not. + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSync)); + EXPECT_FALSE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSync)); + EXPECT_TRUE(identity_manager()->HasPrimaryAccount(ConsentLevel::kSignin)); + EXPECT_EQ(first_account, + identity_manager()->GetPrimaryAccountInfo(ConsentLevel::kSignin)); + + EXPECT_EQ(2U, observer().events().size()); + event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(second_account, event.GetPreviousState().primary_account); + EXPECT_EQ(second_account, event.GetCurrentState().primary_account); + + event = observer().events()[1]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kNone, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kSet, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(second_account, event.GetPreviousState().primary_account); + EXPECT_EQ(first_account, event.GetCurrentState().primary_account); +} + +TEST_F(SigninManagerTest, ClearPrimaryAccountAndSignOut) { + AccountInfo account = MakeSyncAccountAvailableWithCookies(kTestEmail); + ExpectSyncPrimaryAccountSetEvent(account); + + identity_test_env()->ClearPrimaryAccount(); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSync)); + EXPECT_EQ(account, event.GetPreviousState().primary_account); + EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty()); +} + +TEST_F(SigninManagerTest, + UnconsentedPrimaryAccountClearedWhenSigninDisallowed) { + // Prerequisite: add an unconsented primary account. + AccountInfo account = MakeAccountAvailableWithCookies(kTestEmail); + ExpectUnconsentedPrimaryAccountSetEvent(account); + + prefs_.SetBoolean(prefs::kSigninAllowed, false); + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE( + identity_manager()->HasPrimaryAccount(signin::ConsentLevel::kSignin)); + + EXPECT_EQ(1U, observer().events().size()); + auto event = observer().events()[0]; + EXPECT_EQ(PrimaryAccountChangeEvent::Type::kCleared, + event.GetEventTypeFor(ConsentLevel::kSignin)); + EXPECT_EQ(account, event.GetPreviousState().primary_account); + EXPECT_TRUE(event.GetCurrentState().primary_account.IsEmpty()); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc new file mode 100644 index 00000000000..1f0c6a9ae36 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater.cc @@ -0,0 +1,72 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/signin_profile_attributes_updater.h" + +#include <string> + +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/common/pref_names.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "google_apis/gaia/gaia_auth_util.h" + +SigninProfileAttributesUpdater::SigninProfileAttributesUpdater( + signin::IdentityManager* identity_manager, + ProfileAttributesStorage* profile_attributes_storage, + const base::FilePath& profile_path, + PrefService* prefs) + : identity_manager_(identity_manager), + profile_attributes_storage_(profile_attributes_storage), + profile_path_(profile_path), + prefs_(prefs) { + DCHECK(identity_manager_); + DCHECK(profile_attributes_storage_); + identity_manager_observation_.Observe(identity_manager_.get()); + + UpdateProfileAttributes(); +} + +SigninProfileAttributesUpdater::~SigninProfileAttributesUpdater() = default; + +void SigninProfileAttributesUpdater::Shutdown() { + identity_manager_observation_.Reset(); +} + +void SigninProfileAttributesUpdater::UpdateProfileAttributes() { + ProfileAttributesEntry* entry = + profile_attributes_storage_->GetProfileAttributesWithPath(profile_path_); + if (!entry) { + return; + } + + CoreAccountInfo account_info = + identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + + bool clear_profile = account_info.IsEmpty(); + + if (account_info.gaia != entry->GetGAIAId() || + !gaia::AreEmailsSame(account_info.email, + base::UTF16ToUTF8(entry->GetUserName()))) { + // Reset prefs. Note: this will also update the |ProfileAttributesEntry|. + prefs_->ClearPref(prefs::kProfileUsingDefaultAvatar); + prefs_->ClearPref(prefs::kProfileUsingGAIAAvatar); + } + + if (clear_profile) { + entry->SetAuthInfo(std::string(), std::u16string(), + /*is_consented_primary_account=*/false); + } else { + entry->SetAuthInfo( + account_info.gaia, base::UTF8ToUTF16(account_info.email), + identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)); + } +} + +void SigninProfileAttributesUpdater::OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) { + UpdateProfileAttributes(); +} diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater.h b/chromium/chrome/browser/signin/signin_profile_attributes_updater.h new file mode 100644 index 00000000000..8e189e8e370 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater.h @@ -0,0 +1,56 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_ + +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/scoped_observation.h" +#include "components/keyed_service/core/keyed_service.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +class ProfileAttributesStorage; + +// This class listens to various signin events and updates the signin-related +// fields of ProfileAttributes. +class SigninProfileAttributesUpdater + : public KeyedService, + public signin::IdentityManager::Observer { + public: + SigninProfileAttributesUpdater( + signin::IdentityManager* identity_manager, + ProfileAttributesStorage* profile_attributes_storage, + const base::FilePath& profile_path, + PrefService* prefs); + + SigninProfileAttributesUpdater(const SigninProfileAttributesUpdater&) = + delete; + SigninProfileAttributesUpdater& operator=( + const SigninProfileAttributesUpdater&) = delete; + + ~SigninProfileAttributesUpdater() override; + + private: + // KeyedService: + void Shutdown() override; + + // Updates the profile attributes on signin and signout events. + void UpdateProfileAttributes(); + + // IdentityManager::Observer: + void OnPrimaryAccountChanged( + const signin::PrimaryAccountChangeEvent& event) override; + + raw_ptr<signin::IdentityManager> identity_manager_; + raw_ptr<ProfileAttributesStorage> profile_attributes_storage_; + const base::FilePath profile_path_; + raw_ptr<PrefService> prefs_; + base::ScopedObservation<signin::IdentityManager, + signin::IdentityManager::Observer> + identity_manager_observation_{this}; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_H_ diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc new file mode 100644 index 00000000000..86c62a2ce6e --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.cc @@ -0,0 +1,53 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/signin_profile_attributes_updater_factory.h" + +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_profile_attributes_updater.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" + +// static +SigninProfileAttributesUpdater* +SigninProfileAttributesUpdaterFactory::GetForProfile(Profile* profile) { + return static_cast<SigninProfileAttributesUpdater*>( + GetInstance()->GetServiceForBrowserContext(profile, true)); +} + +// static +SigninProfileAttributesUpdaterFactory* +SigninProfileAttributesUpdaterFactory::GetInstance() { + return base::Singleton<SigninProfileAttributesUpdaterFactory>::get(); +} + +SigninProfileAttributesUpdaterFactory::SigninProfileAttributesUpdaterFactory() + : BrowserContextKeyedServiceFactory( + "SigninProfileAttributesUpdater", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(IdentityManagerFactory::GetInstance()); +} + +SigninProfileAttributesUpdaterFactory:: + ~SigninProfileAttributesUpdaterFactory() {} + +KeyedService* SigninProfileAttributesUpdaterFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + // Some tests don't have a ProfileManager, disable this service. + if (!g_browser_process->profile_manager()) + return nullptr; + + return new SigninProfileAttributesUpdater( + IdentityManagerFactory::GetForProfile(profile), + &g_browser_process->profile_manager()->GetProfileAttributesStorage(), + profile->GetPath(), profile->GetPrefs()); +} + +bool SigninProfileAttributesUpdaterFactory::ServiceIsCreatedWithBrowserContext() + const { + return true; +} diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h new file mode 100644 index 00000000000..78f990abaf8 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_factory.h @@ -0,0 +1,42 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +class Profile; +class SigninProfileAttributesUpdater; + +class SigninProfileAttributesUpdaterFactory + : public BrowserContextKeyedServiceFactory { + public: + // Returns nullptr if this profile cannot have a + // SigninProfileAttributesUpdater (for example, if |profile| is incognito). + static SigninProfileAttributesUpdater* GetForProfile(Profile* profile); + + // Returns an instance of the factory singleton. + static SigninProfileAttributesUpdaterFactory* GetInstance(); + + SigninProfileAttributesUpdaterFactory( + const SigninProfileAttributesUpdaterFactory&) = delete; + SigninProfileAttributesUpdaterFactory& operator=( + const SigninProfileAttributesUpdaterFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits< + SigninProfileAttributesUpdaterFactory>; + + SigninProfileAttributesUpdaterFactory(); + ~SigninProfileAttributesUpdaterFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; + bool ServiceIsCreatedWithBrowserContext() const override; +}; + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROFILE_ATTRIBUTES_UPDATER_FACTORY_H_ diff --git a/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc b/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc new file mode 100644 index 00000000000..01430a350b0 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_profile_attributes_updater_unittest.cc @@ -0,0 +1,224 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/signin_profile_attributes_updater.h" + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/memory/raw_ptr.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/signin/public/identity_manager/identity_test_environment.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/sync_preferences/pref_service_syncable.h" +#include "content/public/test/browser_task_environment.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { +#if !BUILDFLAG(IS_CHROMEOS_ASH) +const char kEmail[] = "example@email.com"; + +void CheckProfilePrefsReset(PrefService* pref_service, + bool expected_using_default_name) { + EXPECT_TRUE(pref_service->GetBoolean(prefs::kProfileUsingDefaultAvatar)); + EXPECT_FALSE(pref_service->GetBoolean(prefs::kProfileUsingGAIAAvatar)); + EXPECT_EQ(expected_using_default_name, + pref_service->GetBoolean(prefs::kProfileUsingDefaultName)); +} + +void CheckProfilePrefsSet(PrefService* pref_service, + bool expected_is_using_default_name) { + EXPECT_FALSE(pref_service->GetBoolean(prefs::kProfileUsingDefaultAvatar)); + EXPECT_TRUE(pref_service->GetBoolean(prefs::kProfileUsingGAIAAvatar)); + EXPECT_EQ(expected_is_using_default_name, + pref_service->GetBoolean(prefs::kProfileUsingDefaultName)); +} + +// Set the prefs to nondefault values. +void SetProfilePrefs(PrefService* pref_service) { + pref_service->SetBoolean(prefs::kProfileUsingDefaultAvatar, false); + pref_service->SetBoolean(prefs::kProfileUsingGAIAAvatar, true); + pref_service->SetBoolean(prefs::kProfileUsingDefaultName, false); + + CheckProfilePrefsSet(pref_service, false); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) +} // namespace + +class SigninProfileAttributesUpdaterTest : public testing::Test { + public: + SigninProfileAttributesUpdaterTest() + : profile_manager_(TestingBrowserProcess::GetGlobal()) {} + + // Recreates |signin_profile_attributes_updater_|. Useful for tests that want + // to set up the updater with specific preconditions. + void RecreateSigninProfileAttributesUpdater() { + signin_profile_attributes_updater_ = + std::make_unique<SigninProfileAttributesUpdater>( + identity_test_env_.identity_manager(), + profile_manager_.profile_attributes_storage(), profile_->GetPath(), + profile_->GetPrefs()); + } + + void SetUp() override { + testing::Test::SetUp(); + + ASSERT_TRUE(profile_manager_.SetUp()); + std::string name = "profile_name"; + profile_ = profile_manager_.CreateTestingProfile( + name, /*prefs=*/nullptr, base::UTF8ToUTF16(name), 0, std::string(), + TestingProfile::TestingFactories()); + + RecreateSigninProfileAttributesUpdater(); + } + + content::BrowserTaskEnvironment task_environment_; + TestingProfileManager profile_manager_; + raw_ptr<TestingProfile> profile_; + signin::IdentityTestEnvironment identity_test_env_; + std::unique_ptr<SigninProfileAttributesUpdater> + signin_profile_attributes_updater_; +}; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +// Tests that the browser state info is updated on signin and signout. +// ChromeOS does not support signout. +TEST_F(SigninProfileAttributesUpdaterTest, SigninSignout) { + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + ASSERT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn); + EXPECT_FALSE(entry->IsSigninRequired()); + + // Signin. + identity_test_env_.MakePrimaryAccountAvailable(kEmail, + signin::ConsentLevel::kSync); + EXPECT_TRUE(entry->IsAuthenticated()); + EXPECT_EQ(signin::GetTestGaiaIdForEmail(kEmail), entry->GetGAIAId()); + EXPECT_EQ(kEmail, base::UTF16ToUTF8(entry->GetUserName())); + + // Signout. + identity_test_env_.ClearPrimaryAccount(); + EXPECT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn); + EXPECT_FALSE(entry->IsSigninRequired()); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +TEST_F(SigninProfileAttributesUpdaterTest, SigninSignoutResetsProfilePrefs) { + PrefService* pref_service = profile_->GetPrefs(); + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + + // Set profile prefs. + CheckProfilePrefsReset(pref_service, true); +#if !defined(OS_ANDROID) + SetProfilePrefs(pref_service); + + // Set UPA should reset profile prefs. + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + "email1@example.com", signin::ConsentLevel::kSignin); + EXPECT_FALSE(entry->IsAuthenticated()); + CheckProfilePrefsReset(pref_service, false); + SetProfilePrefs(pref_service); + // Signout should reset profile prefs. + identity_test_env_.ClearPrimaryAccount(); + CheckProfilePrefsReset(pref_service, false); +#endif // !defined(OS_ANDROID) + + SetProfilePrefs(pref_service); + // Set primary account should reset profile prefs. + AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable( + "primary@example.com", signin::ConsentLevel::kSync); + CheckProfilePrefsReset(pref_service, false); + SetProfilePrefs(pref_service); + // Disabling sync should reset profile prefs. + identity_test_env_.ClearPrimaryAccount(); + CheckProfilePrefsReset(pref_service, false); +} + +#if !defined(OS_ANDROID) +TEST_F(SigninProfileAttributesUpdaterTest, + EnablingSyncWithUPAAccountShouldNotResetProfilePrefs) { + PrefService* pref_service = profile_->GetPrefs(); + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + // Set UPA. + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + "email1@example.com", signin::ConsentLevel::kSignin); + EXPECT_FALSE(entry->IsAuthenticated()); + SetProfilePrefs(pref_service); + // Set primary account to be the same as the UPA. + // Given it is the same account, profile prefs should keep the same state. + identity_test_env_.SetPrimaryAccount(account_info.email, + signin::ConsentLevel::kSync); + EXPECT_TRUE(entry->IsAuthenticated()); + CheckProfilePrefsSet(pref_service, false); + identity_test_env_.ClearPrimaryAccount(); + CheckProfilePrefsReset(pref_service, false); +} + +TEST_F(SigninProfileAttributesUpdaterTest, + EnablingSyncWithDifferentAccountThanUPAResetsProfilePrefs) { + PrefService* pref_service = profile_->GetPrefs(); + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + "email1@example.com", signin::ConsentLevel::kSignin); + EXPECT_FALSE(entry->IsAuthenticated()); + SetProfilePrefs(pref_service); + // Set primary account to a different account than the UPA. + AccountInfo primary_account = identity_test_env_.MakePrimaryAccountAvailable( + "primary@example.com", signin::ConsentLevel::kSync); + EXPECT_TRUE(entry->IsAuthenticated()); + CheckProfilePrefsReset(pref_service, false); +} +#endif // !defined(OS_ANDROID) + +class SigninProfileAttributesUpdaterWithForceSigninTest + : public SigninProfileAttributesUpdaterTest { + public: + SigninProfileAttributesUpdaterWithForceSigninTest() + : forced_signin_setter_(true) {} + + private: + signin_util::ScopedForceSigninSetterForTesting forced_signin_setter_; +}; + +TEST_F(SigninProfileAttributesUpdaterWithForceSigninTest, IsSigninRequired) { + ProfileAttributesEntry* entry = + profile_manager_.profile_attributes_storage() + ->GetProfileAttributesWithPath(profile_->GetPath()); + ASSERT_NE(entry, nullptr); + EXPECT_FALSE(entry->IsAuthenticated()); + EXPECT_TRUE(entry->IsSigninRequired()); + + AccountInfo account_info = identity_test_env_.MakePrimaryAccountAvailable( + kEmail, signin::ConsentLevel::kSync); + + EXPECT_TRUE(entry->IsAuthenticated()); + EXPECT_EQ(signin::GetTestGaiaIdForEmail(kEmail), entry->GetGAIAId()); + EXPECT_EQ(kEmail, base::UTF16ToUTF8(entry->GetUserName())); + + identity_test_env_.ClearPrimaryAccount(); + EXPECT_EQ(entry->GetSigninState(), SigninState::kNotSignedIn); + EXPECT_TRUE(entry->IsSigninRequired()); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) diff --git a/chromium/chrome/browser/signin/signin_promo.cc b/chromium/chrome/browser/signin/signin_promo.cc new file mode 100644 index 00000000000..9315cb8a924 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo.cc @@ -0,0 +1,143 @@ +// Copyright 2013 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. + +#include "chrome/browser/signin/signin_promo.h" + +#include "base/strings/string_number_conversions.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/google/google_brand.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/signin_promo_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/url_constants.h" +#include "components/google/core/common/google_util.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/storage_partition_config.h" +#include "extensions/browser/guest_view/web_view/web_view_guest.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/base/url_util.h" +#include "url/gurl.h" + +#if defined(OS_WIN) +#include "base/win/windows_version.h" +#endif + +namespace signin { + +const char kSignInPromoQueryKeyAccessPoint[] = "access_point"; +const char kSignInPromoQueryKeyAutoClose[] = "auto_close"; +const char kSignInPromoQueryKeyForceKeepData[] = "force_keep_data"; +const char kSignInPromoQueryKeyReason[] = "reason"; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +GURL GetEmbeddedPromoURL(signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + bool auto_close) { + CHECK_LT(static_cast<int>(access_point), + static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_MAX)); + CHECK_NE(static_cast<int>(access_point), + static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN)); + CHECK_LE(static_cast<int>(reason), + static_cast<int>(signin_metrics::Reason::kMaxValue)); + CHECK_NE(static_cast<int>(reason), + static_cast<int>(signin_metrics::Reason::kUnknownReason)); + + GURL url(chrome::kChromeUIChromeSigninURL); + url = net::AppendQueryParameter( + url, signin::kSignInPromoQueryKeyAccessPoint, + base::NumberToString(static_cast<int>(access_point))); + url = + net::AppendQueryParameter(url, signin::kSignInPromoQueryKeyReason, + base::NumberToString(static_cast<int>(reason))); + if (auto_close) { + url = net::AppendQueryParameter(url, signin::kSignInPromoQueryKeyAutoClose, + "1"); + } + return url; +} + +GURL GetEmbeddedReauthURLWithEmail(signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + const std::string& email) { + GURL url = GetEmbeddedPromoURL(access_point, reason, /*auto_close=*/true); + url = net::AppendQueryParameter(url, "email", email); + url = net::AppendQueryParameter(url, "validateEmail", "1"); + return net::AppendQueryParameter(url, "readOnlyEmail", "1"); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +GURL GetChromeSyncURLForDice(const std::string& email, + const std::string& continue_url) { + GURL url = GaiaUrls::GetInstance()->signin_chrome_sync_dice(); + if (!email.empty()) + url = net::AppendQueryParameter(url, "email_hint", email); + if (!continue_url.empty()) + url = net::AppendQueryParameter(url, "continue", continue_url); + return url; +} + +GURL GetAddAccountURLForDice(const std::string& email, + const std::string& continue_url) { + GURL url = GaiaUrls::GetInstance()->add_account_url(); + if (!email.empty()) + url = net::AppendQueryParameter(url, "Email", email); + if (!continue_url.empty()) + url = net::AppendQueryParameter(url, "continue", continue_url); + return url; +} + +content::StoragePartition* GetSigninPartition( + content::BrowserContext* browser_context) { + const auto signin_partition_config = content::StoragePartitionConfig::Create( + browser_context, "chrome-signin", /* partition_name= */ "", + /* in_memory= */ true); + return browser_context->GetStoragePartition(signin_partition_config); +} + +signin_metrics::AccessPoint GetAccessPointForEmbeddedPromoURL(const GURL& url) { + std::string value; + if (!net::GetValueForKeyInQuery(url, kSignInPromoQueryKeyAccessPoint, + &value)) { + return signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN; + } + + int access_point = -1; + base::StringToInt(value, &access_point); + if (access_point < + static_cast<int>( + signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE) || + access_point >= + static_cast<int>(signin_metrics::AccessPoint::ACCESS_POINT_MAX)) { + return signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN; + } + + return static_cast<signin_metrics::AccessPoint>(access_point); +} + +signin_metrics::Reason GetSigninReasonForEmbeddedPromoURL(const GURL& url) { + std::string value; + if (!net::GetValueForKeyInQuery(url, kSignInPromoQueryKeyReason, &value)) + return signin_metrics::Reason::kUnknownReason; + + int reason = -1; + base::StringToInt(value, &reason); + if (reason < + static_cast<int>(signin_metrics::Reason::kSigninPrimaryAccount) || + reason > static_cast<int>(signin_metrics::Reason::kMaxValue)) { + return signin_metrics::Reason::kUnknownReason; + } + + return static_cast<signin_metrics::Reason>(reason); +} + +void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterIntegerPref(prefs::kDiceSigninUserMenuPromoCount, 0); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/signin_promo.h b/chromium/chrome/browser/signin/signin_promo.h new file mode 100644 index 00000000000..36cbc05b7c4 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo.h @@ -0,0 +1,83 @@ +// Copyright 2013 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_ + +#include <string> + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "components/signin/public/base/signin_metrics.h" + +class GURL; + +namespace content { +class BrowserContext; +class StoragePartition; +} // namespace content + +namespace user_prefs { +class PrefRegistrySyncable; +} + +// Utility functions for sign in promos. +namespace signin { + +extern const char kSignInPromoQueryKeyAccessPoint[]; +// TODO(https://crbug.com/1205147): Auto close is unused. Remove it. +extern const char kSignInPromoQueryKeyAutoClose[]; +extern const char kSignInPromoQueryKeyForceKeepData[]; +extern const char kSignInPromoQueryKeyReason[]; + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +// These functions are only used to unlock the profile from the desktop user +// manager and the windows credential provider. + +// Returns the sign in promo URL that can be used in a modal dialog with +// the given arguments in the query. +// |access_point| indicates where the sign in is being initiated. +// |reason| indicates the purpose of using this URL. +// |auto_close| whether to close the sign in promo automatically when done. +GURL GetEmbeddedPromoURL(signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + bool auto_close); + +// Returns a sign in promo URL specifically for reauthenticating |email| that +// can be used in a modal dialog. +GURL GetEmbeddedReauthURLWithEmail(signin_metrics::AccessPoint access_point, + signin_metrics::Reason reason, + const std::string& email); +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +// Returns the URL to be used to signin and turn on Sync when DICE is enabled. +// If email is not empty, then it will pass email as hint to the page so that it +// will be autofilled by Gaia. +// If |continue_url| is empty, this may redirect to myaccount. +GURL GetChromeSyncURLForDice(const std::string& email, + const std::string& continue_url); + +// Returns the URL to be used to add (secondary) account when DICE is enabled. +// If email is not empty, then it will pass email as hint to the page so that it +// will be autofilled by Gaia. +// If |continue_url| is empty, this may redirect to myaccount. +GURL GetAddAccountURLForDice(const std::string& email, + const std::string& continue_url); + +// Gets the partition for the embedded sign in frame/webview. +content::StoragePartition* GetSigninPartition( + content::BrowserContext* browser_context); + +// Gets the access point from the query portion of the sign in promo URL. +signin_metrics::AccessPoint GetAccessPointForEmbeddedPromoURL(const GURL& url); + +// Gets the sign in reason from the query portion of the sign in promo URL. +signin_metrics::Reason GetSigninReasonForEmbeddedPromoURL(const GURL& url); + +// Registers the preferences the Sign In Promo needs. +void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_H_ diff --git a/chromium/chrome/browser/signin/signin_promo_unittest.cc b/chromium/chrome/browser/signin/signin_promo_unittest.cc new file mode 100644 index 00000000000..c6dc4054af2 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo_unittest.cc @@ -0,0 +1,56 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/signin_promo.h" + +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/common/webui_url_constants.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace signin { + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +TEST(SigninPromoTest, TestPromoURL) { + GURL::Replacements replace_query; + replace_query.SetQueryStr("access_point=0&reason=0&auto_close=1"); + EXPECT_EQ( + GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query), + GetEmbeddedPromoURL(signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE, + signin_metrics::Reason::kSigninPrimaryAccount, true)); + replace_query.SetQueryStr("access_point=15&reason=1"); + EXPECT_EQ( + GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query), + GetEmbeddedPromoURL( + signin_metrics::AccessPoint::ACCESS_POINT_SIGNIN_PROMO, + signin_metrics::Reason::kAddSecondaryAccount, false)); +} + +TEST(SigninPromoTest, TestReauthURL) { + GURL::Replacements replace_query; + replace_query.SetQueryStr( + "access_point=0&reason=6&auto_close=1" + "&email=example%40domain.com&validateEmail=1" + "&readOnlyEmail=1"); + EXPECT_EQ( + GURL(chrome::kChromeUIChromeSigninURL).ReplaceComponents(replace_query), + GetEmbeddedReauthURLWithEmail( + signin_metrics::AccessPoint::ACCESS_POINT_START_PAGE, + signin_metrics::Reason::kFetchLstOnly, "example@domain.com")); +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +TEST(SigninPromoTest, SigninURLForDice) { + EXPECT_EQ( + "https://accounts.google.com/signin/chrome/sync?ssp=1&" + "email_hint=email%40gmail.com&continue=https%3A%2F%2Fcontinue_url%2F", + GetChromeSyncURLForDice("email@gmail.com", "https://continue_url/")); + EXPECT_EQ( + "https://accounts.google.com/AddSession?" + "Email=email%40gmail.com&continue=https%3A%2F%2Fcontinue_url%2F", + GetAddAccountURLForDice("email@gmail.com", "https://continue_url/")); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/signin_promo_util.cc b/chromium/chrome/browser/signin/signin_promo_util.cc new file mode 100644 index 00000000000..4497bbf6b72 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo_util.cc @@ -0,0 +1,49 @@ +// Copyright 2016 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. + +#include "chrome/browser/signin/signin_promo_util.h" + +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "net/base/network_change_notifier.h" + +namespace signin { + +bool ShouldShowPromo(Profile* profile) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + // There's no need to show the sign in promo on cros since cros users are + // already logged in. + return false; +#else + + // Don't bother if we don't have any kind of network connection. + if (net::NetworkChangeNotifier::IsOffline()) + return false; + + // Consider original profile even if an off-the-record profile was + // passed to this method as sign-in state is only defined for the + // primary profile. + Profile* original_profile = profile->GetOriginalProfile(); + + // Don't show for supervised child profiles. + if (original_profile->IsChild()) + return false; + + // Don't show if sign-in is not allowed. + if (!original_profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed)) + return false; + + // Display the signin promo if the user is not signed in. + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(original_profile); + return !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync); +#endif +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/signin_promo_util.h b/chromium/chrome/browser/signin/signin_promo_util.h new file mode 100644 index 00000000000..36a68d39e00 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_promo_util.h @@ -0,0 +1,18 @@ +// Copyright 2016 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_ + +class Profile; + +namespace signin { + +// Returns true if the sign in promo should be visible. +// |profile| is the profile of the tab the promo would be shown on. +bool ShouldShowPromo(Profile* profile); + +} // namespace signin + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_PROMO_UTIL_H_ diff --git a/chromium/chrome/browser/signin/signin_ui_util.cc b/chromium/chrome/browser/signin/signin_ui_util.cc new file mode 100644 index 00000000000..18c541f96a2 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_ui_util.cc @@ -0,0 +1,608 @@ +// Copyright (c) 2013 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. + +#include "chrome/browser/signin/signin_ui_util.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/callback_helpers.h" +#include "base/feature_list.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/metrics/user_metrics.h" +#include "base/notreached.h" +#include "base/strings/strcat.h" +#include "base/strings/string_util.h" +#include "base/strings/sys_string_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "base/supports_user_data.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/feature_engagement/tracker_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_navigator.h" +#include "chrome/browser/ui/browser_navigator_params.h" +#include "chrome/browser/ui/chrome_pages.h" +#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/common/pref_names.h" +#include "components/feature_engagement/public/tracker.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_utils.h" +#include "third_party/re2/src/re2/re2.h" +#include "ui/gfx/font_list.h" +#include "ui/gfx/text_elider.h" + +#if BUILDFLAG(IS_CHROMEOS_ASH) +#include "chrome/browser/ash/profiles/profile_helper.h" +#endif + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "components/account_manager_core/account_manager_facade.h" +#include "components/account_manager_core/chromeos/account_manager_facade_factory.h" +#endif + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "chrome/browser/signin/account_consistency_mode_manager.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#endif + +namespace { + +// Key for storing animated identity per-profile data. +const char kAnimatedIdentityKeyName[] = "animated_identity_user_data"; + +constexpr base::TimeDelta kDelayForCrossWindowAnimationReplay = + base::Seconds(5); + +// UserData attached to the user profile, keeping track of the last time the +// animation was shown to the user. +class AvatarButtonUserData : public base::SupportsUserData::Data { + public: + ~AvatarButtonUserData() override = default; + + // Returns the last time the animated identity was shown. Returns the null + // time if it was never shown. + static base::TimeTicks GetAnimatedIdentityLastShown(Profile* profile) { + DCHECK(profile); + AvatarButtonUserData* data = GetForProfile(profile); + if (!data) + return base::TimeTicks(); + return data->animated_identity_last_shown_; + } + + // Sets the time when the animated identity was shown. + static void SetAnimatedIdentityLastShown(Profile* profile, + base::TimeTicks time) { + DCHECK(!time.is_null()); + GetOrCreateForProfile(profile)->animated_identity_last_shown_ = time; + } + + private: + // Returns nullptr if there is no AvatarButtonUserData attached to the + // profile. + static AvatarButtonUserData* GetForProfile(Profile* profile) { + return static_cast<AvatarButtonUserData*>( + profile->GetUserData(kAnimatedIdentityKeyName)); + } + + // Never returns nullptr. + static AvatarButtonUserData* GetOrCreateForProfile(Profile* profile) { + DCHECK(profile); + AvatarButtonUserData* existing_data = GetForProfile(profile); + if (existing_data) + return existing_data; + + auto new_data = std::make_unique<AvatarButtonUserData>(); + auto* new_data_ptr = new_data.get(); + profile->SetUserData(kAnimatedIdentityKeyName, std::move(new_data)); + return new_data_ptr; + } + + base::TimeTicks animated_identity_last_shown_; +}; + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +void CreateDiceTurnSyncOnHelper( + Profile* profile, + Browser* browser, + signin_metrics::AccessPoint signin_access_point, + signin_metrics::PromoAction signin_promo_action, + signin_metrics::Reason signin_reason, + const CoreAccountId& account_id, + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode) { + // DiceTurnSyncOnHelper is suicidal (it will delete itself once it finishes + // enabling sync). + new DiceTurnSyncOnHelper(profile, browser, signin_access_point, + signin_promo_action, signin_reason, account_id, + signin_aborted_mode); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +std::string GetReauthAccessPointHistogramSuffix( + signin_metrics::ReauthAccessPoint access_point) { + switch (access_point) { + case signin_metrics::ReauthAccessPoint::kUnknown: + NOTREACHED(); + return std::string(); + case signin_metrics::ReauthAccessPoint::kAutofillDropdown: + return "ToFillPassword"; + case signin_metrics::ReauthAccessPoint::kPasswordSaveBubble: + return "ToSaveOrUpdatePassword"; + case signin_metrics::ReauthAccessPoint::kPasswordSettings: + return "ToManageInSettings"; + case signin_metrics::ReauthAccessPoint::kGeneratePasswordDropdown: + case signin_metrics::ReauthAccessPoint::kGeneratePasswordContextMenu: + return "ToGeneratePassword"; + case signin_metrics::ReauthAccessPoint::kPasswordMoveBubble: + return "ToMovePassword"; + case signin_metrics::ReauthAccessPoint::kPasswordSaveLocallyBubble: + return "ToSavePasswordLocallyThenMove"; + } +} + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +account_manager::AccountManagerFacade::AccountAdditionSource +GetAccountReauthSourceFromAccessPoint( + signin_metrics::AccessPoint access_point) { + switch (access_point) { + case signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN: + return account_manager::AccountManagerFacade::AccountAdditionSource:: + kAvatarBubbleReauthAccountButton; + default: + NOTREACHED() << "Reauth is requested from an unknown access point " + << static_cast<int>(access_point); + return account_manager::AccountManagerFacade::AccountAdditionSource:: + kMaxValue; + } +} +#endif + +} // namespace + +namespace signin_ui_util { + +std::u16string GetAuthenticatedUsername(Profile* profile) { + DCHECK(profile); + std::string user_display_name; + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + user_display_name = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync) + .email; +#if BUILDFLAG(IS_CHROMEOS_ASH) + // See https://crbug.com/994798 for details. + user_manager::User* user = + chromeos::ProfileHelper::Get()->GetUserByProfile(profile); + // |user| may be null in tests. + if (user) + user_display_name = user->GetDisplayEmail(); +#endif // BUILDFLAG(IS_CHROMEOS_ASH) + } + + return base::UTF8ToUTF16(user_display_name); +} + +void InitializePrefsForProfile(Profile* profile) { + if (profile->IsNewProfile()) { + // Suppresses the upgrade tutorial for a new profile. + profile->GetPrefs()->SetInteger(prefs::kProfileAvatarTutorialShown, + kUpgradeWelcomeTutorialShowMax + 1); + } +} + +void ShowSigninErrorLearnMorePage(Profile* profile) { + static const char kSigninErrorLearnMoreUrl[] = + "https://support.google.com/chrome/answer/1181420?"; + NavigateParams params(profile, GURL(kSigninErrorLearnMoreUrl), + ui::PAGE_TRANSITION_LINK); + params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; + Navigate(¶ms); +} + +void ShowReauthForPrimaryAccountWithAuthError( + Browser* browser, + signin_metrics::AccessPoint access_point) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + // On ChromeOS, sync errors are fixed by re-signing into the OS. + NOTREACHED(); +#elif BUILDFLAG(IS_CHROMEOS_LACROS) + internal::ShowReauthForPrimaryAccountWithAuthErrorLacros( + browser, access_point, + ::GetAccountManagerFacade(browser->profile()->GetPath().value())); +#else + browser->signin_view_controller()->ShowSignin( + profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH, access_point); +#endif +} + +void ShowExtensionSigninPrompt(Profile* profile, + bool enable_sync, + const std::string& email_hint) { +#if BUILDFLAG(IS_CHROMEOS_ASH) + NOTREACHED(); +#else + internal::ShowExtensionSigninPrompt( + profile, +#if BUILDFLAG(IS_CHROMEOS_LACROS) + ::GetAccountManagerFacade(profile->GetPath().value()), +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + enable_sync, email_hint); +#endif // BUILDFLAG(IS_CHROMEOS_ASH) +} + +namespace internal { +#if BUILDFLAG(IS_CHROMEOS_LACROS) +void ShowReauthForPrimaryAccountWithAuthErrorLacros( + Browser* browser, + signin_metrics::AccessPoint access_point, + account_manager::AccountManagerFacade* account_manager_facade) { + Profile* profile = browser->profile(); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + CoreAccountInfo primary_account_info = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + DCHECK(!primary_account_info.IsEmpty()); + DCHECK(identity_manager->HasAccountWithRefreshTokenInPersistentErrorState( + primary_account_info.account_id)); + account_manager_facade->ShowReauthAccountDialog( + GetAccountReauthSourceFromAccessPoint(access_point), + primary_account_info.email); +} +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +void ShowExtensionSigninPrompt( + Profile* profile, +#if BUILDFLAG(IS_CHROMEOS_LACROS) + account_manager::AccountManagerFacade* account_manager_facade, +#endif + bool enable_sync, + const std::string& email_hint) { + // There is no sign-in flow for guest or system profile. + if (profile->IsGuestSession() || profile->IsSystemProfile()) + return; + // Locked profile should be unlocked with UserManager only. + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile->GetPath()); + if (entry && entry->IsSigninRequired()) { + return; + } + + // This may be called in incognito. Redirect to the original profile. + profile = profile->GetOriginalProfile(); + +#if BUILDFLAG(IS_CHROMEOS_LACROS) + // There is no sign-in without Mirror. + if (!AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile)) + return; + + if (email_hint.empty()) { + // Add a new account. + // TODO(https://crbug.com/1260291): add support for signed out profiles. + NOTREACHED() << "Lacros doesn't support signed-out profiles yet."; + return; + } + + // Re-authenticate an existing account. + account_manager_facade->ShowReauthAccountDialog( + account_manager::AccountManagerFacade::AccountAdditionSource:: + kChromeExtensionReauth, + email_hint); +#elif BUILDFLAG(ENABLE_DICE_SUPPORT) + chrome::ScopedTabbedBrowserDisplayer displayer(profile); + Browser* browser = displayer.browser(); + + // Cannot sign in if browser cannot be displayed. + if (!browser) + return; + + if (enable_sync) { + // Set a primary account. + browser->signin_view_controller()->ShowDiceEnableSyncTab( + signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS, + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO, email_hint); + } else { + // Add an account to the web without setting a primary account. + browser->signin_view_controller()->ShowDiceAddAccountTab( + signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS, email_hint); + } +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) +} +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) + +} // namespace internal + +void EnableSyncFromSingleAccountPromo( + Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point) { + EnableSyncFromMultiAccountPromo(browser, account, access_point, + /*is_default_promo_account=*/true); +} + +void EnableSyncFromMultiAccountPromo(Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point, + bool is_default_promo_account) { +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + internal::EnableSyncFromPromo(browser, account, access_point, + is_default_promo_account, + base::BindOnce(&CreateDiceTurnSyncOnHelper)); +#else + NOTREACHED(); +#endif +} + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +namespace internal { +void EnableSyncFromPromo( + Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point, + bool is_default_promo_account, + base::OnceCallback< + void(Profile* profile, + Browser* browser, + signin_metrics::AccessPoint signin_access_point, + signin_metrics::PromoAction signin_promo_action, + signin_metrics::Reason signin_reason, + const CoreAccountId& account_id, + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode)> + create_dice_turn_sync_on_helper_callback) { + DCHECK(browser); + DCHECK_NE(signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN, access_point); + Profile* profile = browser->profile(); + DCHECK(!profile->IsOffTheRecord()); + + if (IdentityManagerFactory::GetForProfile(profile)->HasPrimaryAccount( + signin::ConsentLevel::kSync)) { + DVLOG(1) << "There is already a primary account."; + return; + } + + if (account.IsEmpty()) { + chrome::ShowBrowserSignin(browser, access_point, + signin::ConsentLevel::kSync); + return; + } + + DCHECK(!account.account_id.empty()); + DCHECK(!account.email.empty()); + DCHECK(AccountConsistencyModeManager::IsDiceEnabledForProfile(profile)); + + signin_metrics::PromoAction promo_action = + is_default_promo_account + ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT + : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT; + + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + bool needs_reauth_before_enable_sync = + !identity_manager->HasAccountWithRefreshToken(account.account_id) || + identity_manager->HasAccountWithRefreshTokenInPersistentErrorState( + account.account_id); + if (needs_reauth_before_enable_sync) { + browser->signin_view_controller()->ShowDiceEnableSyncTab( + access_point, promo_action, account.email); + return; + } + + signin_metrics::LogSigninAccessPointStarted(access_point, promo_action); + signin_metrics::RecordSigninUserActionForAccessPoint(access_point, + promo_action); + std::move(create_dice_turn_sync_on_helper_callback) + .Run(profile, browser, access_point, promo_action, + signin_metrics::Reason::kSigninPrimaryAccount, account.account_id, + DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT); +} +} // namespace internal + +std::vector<AccountInfo> GetAccountsForDicePromos(Profile* profile) { + // Fetch account ids for accounts that have a token. + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + std::vector<AccountInfo> accounts_with_tokens = + identity_manager->GetExtendedAccountInfoForAccountsWithRefreshToken(); + + // Compute the default account. + CoreAccountId default_account_id = + identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSignin); + + // Fetch account information for each id and make sure that the first account + // in the list matches the unconsented primary account (if available). + std::vector<AccountInfo> accounts; + for (auto& account_info : accounts_with_tokens) { + DCHECK(!account_info.IsEmpty()); + if (!signin::IsUsernameAllowedByPatternFromPrefs( + g_browser_process->local_state(), account_info.email)) { + continue; + } + if (account_info.account_id == default_account_id) + accounts.insert(accounts.begin(), std::move(account_info)); + else + accounts.push_back(std::move(account_info)); + } + return accounts; +} + +AccountInfo GetSingleAccountForDicePromos(Profile* profile) { + std::vector<AccountInfo> accounts = GetAccountsForDicePromos(profile); + if (!accounts.empty()) + return accounts[0]; + return AccountInfo(); +} + +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +std::u16string GetShortProfileIdentityToDisplay( + const ProfileAttributesEntry& profile_attributes_entry, + Profile* profile) { + DCHECK(profile); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + CoreAccountInfo core_info = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin); + // If there's no unconsented primary account, simply return the name of the + // profile according to profile attributes. + if (core_info.IsEmpty()) + return profile_attributes_entry.GetName(); + + AccountInfo extended_info = + identity_manager->FindExtendedAccountInfoByAccountId( + core_info.account_id); + // If there's no given name available, return the user email. + if (extended_info.given_name.empty()) + return base::UTF8ToUTF16(core_info.email); + + return base::UTF8ToUTF16(extended_info.given_name); +} + +std::string GetAllowedDomain(std::string signin_pattern) { + std::vector<std::string> splitted_signin_pattern = base::SplitString( + signin_pattern, "@", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); + + // There are more than one '@'s in the pattern. + if (splitted_signin_pattern.size() != 2) + return std::string(); + + std::string domain = splitted_signin_pattern[1]; + + // Trims tailing '$' if existed. + if (!domain.empty() && domain.back() == '$') + domain.pop_back(); + + // Trims tailing '\E' if existed. + if (domain.size() > 1 && + base::EndsWith(domain, "\\E", base::CompareCase::SENSITIVE)) + domain.erase(domain.size() - 2); + + // Check if there is any special character in the domain. Note that + // jsmith@[192.168.2.1] is not supported. + if (!re2::RE2::FullMatch(domain, "[a-zA-Z0-9\\-.]+")) + return std::string(); + + return domain; +} + +bool ShouldShowAnimatedIdentityOnOpeningWindow( + const ProfileAttributesStorage& profile_attributes_storage, + Profile* profile) { + DCHECK(profile); + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + DCHECK(identity_manager->AreRefreshTokensLoaded()); + + base::TimeTicks animation_last_shown = + AvatarButtonUserData::GetAnimatedIdentityLastShown(profile); + // When a new window is created, only show the animation if it was never shown + // for this profile, or if it was shown in another window in the last few + // seconds (because the user may have missed it). + if (!animation_last_shown.is_null() && + base::TimeTicks::Now() - animation_last_shown > + kDelayForCrossWindowAnimationReplay) { + return false; + } + + // Show the user identity for users with multiple profiles. + if (profile_attributes_storage.GetNumberOfProfiles() > 1) { + return true; + } + + // Show the user identity for users with multiple signed-in accounts. + return identity_manager->GetAccountsWithRefreshTokens().size() > 1; +} + +void RecordAnimatedIdentityTriggered(Profile* profile) { + AvatarButtonUserData::SetAnimatedIdentityLastShown(profile, + base::TimeTicks::Now()); +} + +void RecordAvatarIconHighlighted(Profile* profile) { + base::RecordAction(base::UserMetricsAction("AvatarToolbarButtonHighlighted")); +} + +void RecordProfileMenuViewShown(Profile* profile) { + base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened")); + if (profile->IsRegularProfile()) { + base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Regular")); + // Record usage for profile switch promo. + feature_engagement::TrackerFactory::GetForBrowserContext(profile) + ->NotifyEvent("profile_menu_shown"); + } else if (profile->IsGuestSession()) { + base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Guest")); + } else if (profile->IsIncognitoProfile()) { + base::RecordAction(base::UserMetricsAction("ProfileMenu_Opened_Incognito")); + } + + base::TimeTicks last_shown = + AvatarButtonUserData::GetAnimatedIdentityLastShown(profile); + if (!last_shown.is_null()) { + base::UmaHistogramLongTimes("Profile.Menu.OpenedAfterAvatarAnimation", + base::TimeTicks::Now() - last_shown); + } +} + +void RecordProfileMenuClick(Profile* profile) { + base::RecordAction( + base::UserMetricsAction("ProfileMenu_ActionableItemClicked")); + if (profile->IsRegularProfile()) { + base::RecordAction( + base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Regular")); + } else if (profile->IsGuestSession()) { + base::RecordAction( + base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Guest")); + } else if (profile->IsIncognitoProfile()) { + base::RecordAction( + base::UserMetricsAction("ProfileMenu_ActionableItemClicked_Incognito")); + } +} + +void RecordTransactionalReauthResult( + signin_metrics::ReauthAccessPoint access_point, + signin::ReauthResult result) { + const char kHistogramName[] = "Signin.TransactionalReauthResult"; + base::UmaHistogramEnumeration(kHistogramName, result); + + std::string access_point_suffix = + GetReauthAccessPointHistogramSuffix(access_point); + if (!access_point_suffix.empty()) { + std::string suffixed_histogram_name = + base::StrCat({kHistogramName, ".", access_point_suffix}); + base::UmaHistogramEnumeration(suffixed_histogram_name, result); + } +} + +void RecordTransactionalReauthUserAction( + signin_metrics::ReauthAccessPoint access_point, + SigninReauthViewController::UserAction user_action) { + const char kHistogramName[] = "Signin.TransactionalReauthUserAction"; + base::UmaHistogramEnumeration(kHistogramName, user_action); + + std::string access_point_suffix = + GetReauthAccessPointHistogramSuffix(access_point); + if (!access_point_suffix.empty()) { + std::string suffixed_histogram_name = + base::StrCat({kHistogramName, ".", access_point_suffix}); + base::UmaHistogramEnumeration(suffixed_histogram_name, user_action); + } +} + +} // namespace signin_ui_util diff --git a/chromium/chrome/browser/signin/signin_ui_util.h b/chromium/chrome/browser/signin/signin_ui_util.h new file mode 100644 index 00000000000..075952e959b --- /dev/null +++ b/chromium/chrome/browser/signin/signin_ui_util.h @@ -0,0 +1,188 @@ +// Copyright (c) 2013 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_ + +#include <string> +#include <vector> + +#include "base/callback_forward.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/signin/reauth_result.h" +#include "chrome/browser/ui/signin_reauth_view_controller.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/base/signin_metrics.h" + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#endif + +struct AccountInfo; +class Browser; +class Profile; +class ProfileAttributesEntry; +class ProfileAttributesStorage; + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +namespace account_manager { +class AccountManagerFacade; +} +#endif + +// Utility functions to gather status information from the various signed in +// services and construct messages suitable for showing in UI. +namespace signin_ui_util { + +// The maximum number of times to show the welcome tutorial for an upgrade user. +const int kUpgradeWelcomeTutorialShowMax = 1; + +// Returns the username of the primary account or an empty string if there is +// no primary account or the account has not consented to browser sync. +std::u16string GetAuthenticatedUsername(Profile* profile); + +// Initializes signin-related preferences. +void InitializePrefsForProfile(Profile* profile); + +// Shows a learn more page for signin errors. +void ShowSigninErrorLearnMorePage(Profile* profile); + +// Shows a reauth page/dialog to reauthanticate a primary account in error +// state. +void ShowReauthForPrimaryAccountWithAuthError( + Browser* browser, + signin_metrics::AccessPoint access_point); + +// Delegates to an existing sign-in tab if one exists. If not, a new sign-in tab +// is created. +void ShowExtensionSigninPrompt(Profile* profile, + bool enable_sync, + const std::string& email_hint); + +namespace internal { +#if BUILDFLAG(IS_CHROMEOS_LACROS) +// Same as `ShowReauthForPrimaryAccountWithAuthError` but with a getter function +// for AccountManagerFacade so that it can be unit tested. +void ShowReauthForPrimaryAccountWithAuthErrorLacros( + Browser* browser, + signin_metrics::AccessPoint access_point, + account_manager::AccountManagerFacade* account_manager_facade); +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +#if !BUILDFLAG(IS_CHROMEOS_ASH) +void ShowExtensionSigninPrompt( + Profile* profile, +#if BUILDFLAG(IS_CHROMEOS_LACROS) + account_manager::AccountManagerFacade* account_manager_facade, +#endif + bool enable_sync, + const std::string& email_hint); +#endif +} // namespace internal + +// This function is used to enable sync for a given account: +// * This function does nothing if the user is already signed in to Chrome. +// * If |account| is empty, then it presents the Chrome sign-in page. +// * If token service has an invalid refreh token for account |account|, +// then it presents the Chrome sign-in page with |account.emil| prefilled. +// * If token service has a valid refresh token for |account|, then it +// enables sync for |account|. +void EnableSyncFromSingleAccountPromo(Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point); + +// This function is used to enable sync for a given account. It has the same +// behavior as |EnableSyncFromSingleAccountPromo()| except that it also logs +// some additional information if the action is started from a promo that +// supports selecting the account that may be used for sync. +// +// |is_default_promo_account| is true if |account| corresponds to the default +// account in the promo. It is ignored if |account| is empty. +void EnableSyncFromMultiAccountPromo(Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point, + bool is_default_promo_account); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +// Returns the list of all accounts that have a token. The unconsented primary +// account will be the first account in the list. +std::vector<AccountInfo> GetAccountsForDicePromos(Profile* profile); + +// Returns single account to use in Dice promos. +AccountInfo GetSingleAccountForDicePromos(Profile* profile); + +#endif + +// Returns the short user identity to display for |profile|. It is based on the +// current unconsented primary account (if exists). +// TODO(crbug.com/1012179): Move this logic into ProfileAttributesEntry once +// AvatarToolbarButton becomes an observer of ProfileAttributesStorage and thus +// ProfileAttributesEntry is up-to-date when AvatarToolbarButton needs it. +std::u16string GetShortProfileIdentityToDisplay( + const ProfileAttributesEntry& profile_attributes_entry, + Profile* profile); + +// Returns the domain of the policy value of RestrictSigninToPattern. Returns +// an empty string if the policy is not set or can not be parsed. The parser +// only supports the policy value that matches [^@]+@[a-zA-Z0-9\-.]+(\\E)?\$?$. +// Also, the parser does not validate the policy value. +std::string GetAllowedDomain(std::string signin_pattern); + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) +namespace internal { +// Same as |EnableSyncFromPromo| but with a callback that creates a +// DiceTurnSyncOnHelper so that it can be unit tested. +void EnableSyncFromPromo( + Browser* browser, + const AccountInfo& account, + signin_metrics::AccessPoint access_point, + bool is_default_promo_account, + base::OnceCallback< + void(Profile* profile, + Browser* browser, + signin_metrics::AccessPoint signin_access_point, + signin_metrics::PromoAction signin_promo_action, + signin_metrics::Reason signin_reason, + const CoreAccountId& account_id, + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode)> + create_dice_turn_sync_on_helper_callback); +} // namespace internal +#endif + +// Returns whether Chrome should show the identity of the user (using a brief +// animation) on opening a new window. IdentityManager's refresh tokens must be +// loaded when this function gets called. +bool ShouldShowAnimatedIdentityOnOpeningWindow( + const ProfileAttributesStorage& profile_attributes_storage, + Profile* profile); + +// Records that the animated identity was shown for the given profile. This is +// used for metrics and to decide whether/when the animation can be shown again. +void RecordAnimatedIdentityTriggered(Profile* profile); + +// Records that the avatar icon was highlighted for the given profile. This is +// used for metrics. +void RecordAvatarIconHighlighted(Profile* profile); + +// Called when the ProfileMenuView is opened. Used for metrics. +void RecordProfileMenuViewShown(Profile* profile); + +// Called when a button/link in the profile menu was clicked. +void RecordProfileMenuClick(Profile* profile); + +// Records the result of a re-auth challenge to finish a transaction (like +// unlocking the account store for passwords). +void RecordTransactionalReauthResult( + signin_metrics::ReauthAccessPoint access_point, + signin::ReauthResult result); + +// Records user action performed in a transactional reauth dialog/tab. +void RecordTransactionalReauthUserAction( + signin_metrics::ReauthAccessPoint access_point, + SigninReauthViewController::UserAction user_action); + +} // namespace signin_ui_util + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_H_ diff --git a/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc b/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc new file mode 100644 index 00000000000..299e45539d6 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_ui_util_browsertest.cc @@ -0,0 +1,85 @@ +// Copyright 2021 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_ + +#include "chrome/browser/signin/signin_ui_util.h" + +#include "base/callback_helpers.h" +#include "base/test/bind.h" +#include "build/buildflag.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "content/public/test/browser_test.h" + +#if !BUILDFLAG(ENABLE_DICE_SUPPORT) +#error This file only contains DICE browser tests for now. +#endif + +namespace signin_ui_util { + +class DiceSigninUiUtilBrowserTest : public InProcessBrowserTest { + public: + DiceSigninUiUtilBrowserTest() = default; + ~DiceSigninUiUtilBrowserTest() override = default; + + Profile* CreateProfile() { + Profile* new_profile = nullptr; + base::RunLoop run_loop; + ProfileManager::CreateMultiProfileAsync( + u"test_profile", /*icon_index=*/0, /*is_hidden=*/false, + base::BindLambdaForTesting( + [&new_profile, &run_loop](Profile* profile, + Profile::CreateStatus status) { + ASSERT_NE(status, Profile::CREATE_STATUS_LOCAL_FAIL); + if (status == Profile::CREATE_STATUS_INITIALIZED) { + new_profile = profile; + run_loop.Quit(); + } + })); + run_loop.Run(); + return new_profile; + } + + private: +}; + +// Tests that `ShowExtensionSigninPrompt()` doesn't crash when it cannot create +// a new browser. Regression test for https://crbug.com/1273370. +IN_PROC_BROWSER_TEST_F(DiceSigninUiUtilBrowserTest, + ShowExtensionSigninPrompt_NoBrowser) { + Profile* new_profile = CreateProfile(); + + // New profile should not have any browser windows. + EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile)); + + ShowExtensionSigninPrompt(new_profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + // `ShowExtensionSigninPrompt()` creates a new browser. + Browser* browser = chrome::FindBrowserWithProfile(new_profile); + ASSERT_TRUE(browser); + EXPECT_EQ(1, browser->tab_strip_model()->count()); + + // Profile deletion closes the browser. + g_browser_process->profile_manager()->ScheduleProfileForDeletion( + new_profile->GetPath(), base::DoNothing()); + ui_test_utils::WaitForBrowserToClose(browser); + EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile)); + + // `ShowExtensionSigninPrompt()` does nothing for deleted profile. + ShowExtensionSigninPrompt(new_profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + EXPECT_FALSE(chrome::FindBrowserWithProfile(new_profile)); +} + +} // namespace signin_ui_util + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UI_UTIL_BROWSERTEST_CC_ diff --git a/chromium/chrome/browser/signin/signin_ui_util_unittest.cc b/chromium/chrome/browser/signin/signin_ui_util_unittest.cc new file mode 100644 index 00000000000..43be4319fcc --- /dev/null +++ b/chromium/chrome/browser/signin/signin_ui_util_unittest.cc @@ -0,0 +1,736 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/signin_ui_util.h" + +#include "base/bind.h" +#include "base/memory/raw_ptr.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/metrics/user_action_tester.h" +#include "base/test/task_environment.h" +#include "build/build_config.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/profiles/profile_attributes_init_params.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" +#include "chrome/browser/signin/signin_promo.h" +#include "chrome/browser/signin/signin_util.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/test/base/browser_with_test_window_test.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "components/account_id/account_id.h" +#include "components/google/core/common/google_util.h" +#include "components/signin/public/base/consent_level.h" +#include "components/signin/public/base/signin_buildflags.h" +#include "components/signin/public/identity_manager/account_info.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "google_apis/gaia/gaia_urls.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +#include "components/account_manager_core/mock_account_manager_facade.h" +#endif + +namespace signin_ui_util { + +namespace { +const char kMainEmail[] = "main_email@example.com"; +const char kMainGaiaID[] = "main_gaia_id"; +const char kSecondaryEmail[] = "secondary_email@example.com"; +const char kSecondaryGaiaID[] = "secondary_gaia_id"; +} // namespace + +class GetAllowedDomainTest : public ::testing::Test {}; + +TEST_F(GetAllowedDomainTest, WithInvalidPattern) { + EXPECT_EQ(std::string(), GetAllowedDomain("email")); + EXPECT_EQ(std::string(), GetAllowedDomain("email@a@b")); + EXPECT_EQ(std::string(), GetAllowedDomain("email@a[b")); + EXPECT_EQ(std::string(), GetAllowedDomain("@$")); + EXPECT_EQ(std::string(), GetAllowedDomain("@\\E$")); + EXPECT_EQ(std::string(), GetAllowedDomain("@\\E$a")); + EXPECT_EQ(std::string(), GetAllowedDomain("email@")); + EXPECT_EQ(std::string(), GetAllowedDomain("@")); + EXPECT_EQ(std::string(), GetAllowedDomain("example@a.com|example@b.com")); + EXPECT_EQ(std::string(), GetAllowedDomain("")); +} + +TEST_F(GetAllowedDomainTest, WithValidPattern) { + EXPECT_EQ("example.com", GetAllowedDomain("email@example.com")); + EXPECT_EQ("example.com", GetAllowedDomain("email@example.com\\E")); + EXPECT_EQ("example.com", GetAllowedDomain("email@example.com$")); + EXPECT_EQ("example.com", GetAllowedDomain("email@example.com\\E$")); + EXPECT_EQ("example.com", GetAllowedDomain("*@example.com\\E$")); + EXPECT_EQ("example.com", GetAllowedDomain(".*@example.com\\E$")); + EXPECT_EQ("example-1.com", GetAllowedDomain("email@example-1.com")); +} + +#if BUILDFLAG(ENABLE_DICE_SUPPORT) + +namespace { + +class SigninUiUtilTestBrowserWindow : public TestBrowserWindow { + public: + SigninUiUtilTestBrowserWindow() = default; + + SigninUiUtilTestBrowserWindow(const SigninUiUtilTestBrowserWindow&) = delete; + SigninUiUtilTestBrowserWindow& operator=( + const SigninUiUtilTestBrowserWindow&) = delete; + + ~SigninUiUtilTestBrowserWindow() override = default; + void set_browser(Browser* browser) { browser_ = browser; } + + void ShowAvatarBubbleFromAvatarButton( + AvatarBubbleMode mode, + signin_metrics::AccessPoint access_point, + bool is_source_keyboard) override { + ASSERT_TRUE(browser_); + // Simulate what |BrowserView| does for a regular Chrome sign-in flow. + browser_->signin_view_controller()->ShowSignin( + profiles::BubbleViewMode::BUBBLE_VIEW_MODE_GAIA_SIGNIN, access_point); + } + + private: + raw_ptr<Browser> browser_ = nullptr; +}; + +} // namespace + +class DiceSigninUiUtilTest : public BrowserWithTestWindowTest { + public: + DiceSigninUiUtilTest() = default; + ~DiceSigninUiUtilTest() override = default; + + struct CreateDiceTurnSyncOnHelperParams { + public: + raw_ptr<Profile> profile = nullptr; + raw_ptr<Browser> browser = nullptr; + signin_metrics::AccessPoint signin_access_point = + signin_metrics::AccessPoint::ACCESS_POINT_MAX; + signin_metrics::PromoAction signin_promo_action = + signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO; + signin_metrics::Reason signin_reason = + signin_metrics::Reason::kUnknownReason; + CoreAccountId account_id; + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode = + DiceTurnSyncOnHelper::SigninAbortedMode::REMOVE_ACCOUNT; + }; + + void CreateDiceTurnSyncOnHelper( + Profile* profile, + Browser* browser, + signin_metrics::AccessPoint signin_access_point, + signin_metrics::PromoAction signin_promo_action, + signin_metrics::Reason signin_reason, + const CoreAccountId& account_id, + DiceTurnSyncOnHelper::SigninAbortedMode signin_aborted_mode) { + create_dice_turn_sync_on_helper_called_ = true; + create_dice_turn_sync_on_helper_params_.profile = profile; + create_dice_turn_sync_on_helper_params_.browser = browser; + create_dice_turn_sync_on_helper_params_.signin_access_point = + signin_access_point; + create_dice_turn_sync_on_helper_params_.signin_promo_action = + signin_promo_action; + create_dice_turn_sync_on_helper_params_.signin_reason = signin_reason; + create_dice_turn_sync_on_helper_params_.account_id = account_id; + create_dice_turn_sync_on_helper_params_.signin_aborted_mode = + signin_aborted_mode; + } + + protected: + // BrowserWithTestWindowTest: + void SetUp() override { + BrowserWithTestWindowTest::SetUp(); + static_cast<SigninUiUtilTestBrowserWindow*>(browser()->window()) + ->set_browser(browser()); + } + + // BrowserWithTestWindowTest: + TestingProfile::TestingFactories GetTestingFactories() override { + return IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories(); + } + + // BrowserWithTestWindowTest: + std::unique_ptr<BrowserWindow> CreateBrowserWindow() override { + return std::make_unique<SigninUiUtilTestBrowserWindow>(); + } + + // Returns the identity manager. + signin::IdentityManager* GetIdentityManager() { + return IdentityManagerFactory::GetForProfile(profile()); + } + + void EnableSync(const AccountInfo& account_info, + bool is_default_promo_account) { + signin_ui_util::internal::EnableSyncFromPromo( + browser(), account_info, access_point_, is_default_promo_account, + base::BindOnce(&DiceSigninUiUtilTest::CreateDiceTurnSyncOnHelper, + base::Unretained(this))); + } + + void ExpectNoSigninStartedHistograms( + const base::HistogramTester& histogram_tester) { + histogram_tester.ExpectTotalCount("Signin.SigninStartedAccessPoint", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + } + + void ExpectOneSigninStartedHistograms( + const base::HistogramTester& histogram_tester, + signin_metrics::PromoAction expected_promo_action) { + histogram_tester.ExpectUniqueSample("Signin.SigninStartedAccessPoint", + access_point_, 1); + switch (expected_promo_action) { + case signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + break; + case signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.WithDefault", access_point_, 1); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + break; + case signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.NotDefault", access_point_, 1); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + break; + case signin_metrics::PromoAction:: + PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", + access_point_, 1); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", 0); + break; + case signin_metrics::PromoAction:: + PROMO_ACTION_NEW_ACCOUNT_EXISTING_ACCOUNT: + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.WithDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NotDefault", 0); + histogram_tester.ExpectTotalCount( + "Signin.SigninStartedAccessPoint.NewAccountNoExistingAccount", 0); + histogram_tester.ExpectUniqueSample( + "Signin.SigninStartedAccessPoint.NewAccountExistingAccount", + access_point_, 1); + break; + } + } + + signin_metrics::AccessPoint access_point_ = + signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE; + + bool create_dice_turn_sync_on_helper_called_ = false; + CreateDiceTurnSyncOnHelperParams create_dice_turn_sync_on_helper_params_; +}; + +TEST_F(DiceSigninUiUtilTest, EnableSyncWithExistingAccount) { + CoreAccountId account_id = + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + for (bool is_default_promo_account : {true, false}) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ(0, user_action_tester.GetActionCount( + "Signin_Signin_FromBookmarkBubble")); + + EnableSync( + GetIdentityManager()->FindExtendedAccountInfoByAccountId(account_id), + is_default_promo_account); + signin_metrics::PromoAction expected_promo_action = + is_default_promo_account + ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT + : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT; + ASSERT_TRUE(create_dice_turn_sync_on_helper_called_); + ExpectOneSigninStartedHistograms(histogram_tester, expected_promo_action); + + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_Signin_FromBookmarkBubble")); + if (is_default_promo_account) { + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_SigninWithDefault_FromBookmarkBubble")); + } else { + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_SigninNotDefault_FromBookmarkBubble")); + } + + // Verify that the helper to enable sync is created with the expected + // params. + EXPECT_EQ(profile(), create_dice_turn_sync_on_helper_params_.profile); + EXPECT_EQ(browser(), create_dice_turn_sync_on_helper_params_.browser); + EXPECT_EQ(account_id, create_dice_turn_sync_on_helper_params_.account_id); + EXPECT_EQ(signin_metrics::AccessPoint::ACCESS_POINT_BOOKMARK_BUBBLE, + create_dice_turn_sync_on_helper_params_.signin_access_point); + EXPECT_EQ(expected_promo_action, + create_dice_turn_sync_on_helper_params_.signin_promo_action); + EXPECT_EQ(signin_metrics::Reason::kSigninPrimaryAccount, + create_dice_turn_sync_on_helper_params_.signin_reason); + EXPECT_EQ(DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT, + create_dice_turn_sync_on_helper_params_.signin_aborted_mode); + } +} + +TEST_F(DiceSigninUiUtilTest, EnableSyncWithAccountThatNeedsReauth) { + AddTab(browser(), GURL("http://example.com")); + CoreAccountId account_id = + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + // Add an account and then put its refresh token into an error state to + // require a reauth before enabling sync. + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + GetIdentityManager(), account_id, + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + + for (bool is_default_promo_account : {true, false}) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ(0, user_action_tester.GetActionCount( + "Signin_Signin_FromBookmarkBubble")); + + EnableSync( + GetIdentityManager()->FindExtendedAccountInfoByAccountId(account_id), + is_default_promo_account); + ASSERT_FALSE(create_dice_turn_sync_on_helper_called_); + + ExpectOneSigninStartedHistograms( + histogram_tester, + is_default_promo_account + ? signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT + : signin_metrics::PromoAction::PROMO_ACTION_NOT_DEFAULT); + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_Signin_FromBookmarkBubble")); + + if (is_default_promo_account) { + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_SigninWithDefault_FromBookmarkBubble")); + } else { + EXPECT_EQ(1, user_action_tester.GetActionCount( + "Signin_SigninNotDefault_FromBookmarkBubble")); + } + + // Verify that the active tab has the correct DICE sign-in URL. + TabStripModel* tab_strip = browser()->tab_strip_model(); + content::WebContents* active_contents = tab_strip->GetActiveWebContents(); + ASSERT_TRUE(active_contents); + EXPECT_EQ(signin::GetChromeSyncURLForDice(kMainEmail, + google_util::kGoogleHomepageURL), + active_contents->GetVisibleURL()); + tab_strip->CloseWebContentsAt( + tab_strip->GetIndexOfWebContents(active_contents), + TabStripModel::CLOSE_USER_GESTURE); + } +} + +TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithNoTab) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ( + 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/); + ASSERT_FALSE(create_dice_turn_sync_on_helper_called_); + + ExpectOneSigninStartedHistograms( + histogram_tester, signin_metrics::PromoAction:: + PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(1, + user_action_tester.GetActionCount( + "Signin_SigninNewAccountNoExistingAccount_FromBookmarkBubble")); + + // Verify that the active tab has the correct DICE sign-in URL. + content::WebContents* active_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + ASSERT_TRUE(active_contents); + EXPECT_EQ( + signin::GetChromeSyncURLForDice("", google_util::kGoogleHomepageURL), + active_contents->GetVisibleURL()); +} + +TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithNoTabWithExisting) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ( + 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/); + ASSERT_FALSE(create_dice_turn_sync_on_helper_called_); + + ExpectOneSigninStartedHistograms( + histogram_tester, + signin_metrics::PromoAction::PROMO_ACTION_NEW_ACCOUNT_EXISTING_ACCOUNT); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(1, + user_action_tester.GetActionCount( + "Signin_SigninNewAccountExistingAccount_FromBookmarkBubble")); +} + +TEST_F(DiceSigninUiUtilTest, EnableSyncForNewAccountWithOneTab) { + base::HistogramTester histogram_tester; + base::UserActionTester user_action_tester; + AddTab(browser(), GURL("http://foo/1")); + + ExpectNoSigninStartedHistograms(histogram_tester); + EXPECT_EQ( + 0, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + EnableSync(AccountInfo(), false /* is_default_promo_account (not used)*/); + ASSERT_FALSE(create_dice_turn_sync_on_helper_called_); + + ExpectOneSigninStartedHistograms( + histogram_tester, signin_metrics::PromoAction:: + PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(1, + user_action_tester.GetActionCount( + "Signin_SigninNewAccountNoExistingAccount_FromBookmarkBubble")); + + // Verify that the active tab has the correct DICE sign-in URL. + content::WebContents* active_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + ASSERT_TRUE(active_contents); + EXPECT_EQ( + signin::GetChromeSyncURLForDice("", google_util::kGoogleHomepageURL), + active_contents->GetVisibleURL()); +} + +TEST_F(DiceSigninUiUtilTest, GetAccountsForDicePromos) { + // Should start off with no accounts. + std::vector<AccountInfo> accounts = GetAccountsForDicePromos(profile()); + EXPECT_TRUE(accounts.empty()); + + // TODO(tangltom): Flesh out this test. +} + +TEST_F(DiceSigninUiUtilTest, MergeDiceSigninTab) { + base::UserActionTester user_action_tester; + EnableSync(AccountInfo(), false); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + // Signin tab is reused. + EnableSync(AccountInfo(), false); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + + // Give focus to a different tab. + TabStripModel* tab_strip = browser()->tab_strip_model(); + ASSERT_EQ(0, tab_strip->active_index()); + GURL other_url = GURL("http://example.com"); + AddTab(browser(), other_url); + tab_strip->ActivateTabAt(0, {TabStripModel::GestureType::kOther}); + ASSERT_EQ(other_url, tab_strip->GetActiveWebContents()->GetVisibleURL()); + ASSERT_EQ(0, tab_strip->active_index()); + + // Extensions re-use the tab but do not take focus. + access_point_ = signin_metrics::AccessPoint::ACCESS_POINT_EXTENSIONS; + EnableSync(AccountInfo(), false); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(0, tab_strip->active_index()); + + // Other access points re-use the tab and take focus. + access_point_ = signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS; + EnableSync(AccountInfo(), false); + EXPECT_EQ( + 1, user_action_tester.GetActionCount("Signin_Signin_FromBookmarkBubble")); + EXPECT_EQ(1, tab_strip->active_index()); +} + +TEST_F(DiceSigninUiUtilTest, ShowReauthTab) { + AddTab(browser(), GURL("http://example.com")); + AccountInfo account_info = signin::MakePrimaryAccountAvailable( + GetIdentityManager(), "foo@example.com", signin::ConsentLevel::kSync); + + // Add an account and then put its refresh token into an error state to + // require a reauth before enabling sync. + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + GetIdentityManager(), account_info.account_id, + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + + signin_ui_util::ShowReauthForPrimaryAccountWithAuthError( + browser(), + signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN); + + // Verify that the active tab has the correct DICE sign-in URL. + TabStripModel* tab_strip = browser()->tab_strip_model(); + content::WebContents* active_contents = tab_strip->GetActiveWebContents(); + ASSERT_TRUE(active_contents); + EXPECT_EQ(signin::GetChromeSyncURLForDice(account_info.email, + google_util::kGoogleHomepageURL), + active_contents->GetVisibleURL()); +} + +TEST_F(DiceSigninUiUtilTest, + ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsTrueForMultiProfiles) { + const char kSecondProfile[] = "SecondProfile"; + const char16_t kSecondProfile16[] = u"SecondProfile"; + const base::FilePath profile_path = + profile_manager()->profiles_dir().AppendASCII(kSecondProfile); + ProfileAttributesInitParams params; + params.profile_path = profile_path; + params.profile_name = kSecondProfile16; + profile_manager()->profile_attributes_storage()->AddProfile( + std::move(params)); + + EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager()->profile_attributes_storage(), profile())); +} + +TEST_F(DiceSigninUiUtilTest, + ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsTrueForMultiSignin) { + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kSecondaryGaiaID, kSecondaryEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager()->profile_attributes_storage(), profile())); + + // The identity can be shown again immediately (which is what happens if there + // is multiple windows at startup). + RecordAnimatedIdentityTriggered(profile()); + EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager()->profile_attributes_storage(), profile())); +} + +TEST_F( + DiceSigninUiUtilTest, + ShouldShowAnimatedIdentityOnOpeningWindow_ReturnsFalseForSingleProfileSingleSignin) { + GetIdentityManager()->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + + EXPECT_FALSE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager()->profile_attributes_storage(), profile())); +} + +TEST_F(DiceSigninUiUtilTest, ShowExtensionSigninPrompt) { + Profile* profile = browser()->profile(); + TabStripModel* tab_strip = browser()->tab_strip_model(); + ShowExtensionSigninPrompt(profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + EXPECT_EQ(1, tab_strip->count()); + // Calling the function again reuses the tab. + ShowExtensionSigninPrompt(profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + EXPECT_EQ(1, tab_strip->count()); + + content::WebContents* tab = tab_strip->GetWebContentsAt(0); + ASSERT_TRUE(tab); + EXPECT_TRUE(base::StartsWith( + tab->GetVisibleURL().spec(), + GaiaUrls::GetInstance()->signin_chrome_sync_dice().spec(), + base::CompareCase::INSENSITIVE_ASCII)); + + // Changing the parameter opens a new tab. + ShowExtensionSigninPrompt(profile, /*enable_sync=*/false, + /*email_hint=*/std::string()); + EXPECT_EQ(2, tab_strip->count()); + // Calling the function again reuses the tab. + ShowExtensionSigninPrompt(profile, /*enable_sync=*/false, + /*email_hint=*/std::string()); + EXPECT_EQ(2, tab_strip->count()); + tab = tab_strip->GetWebContentsAt(1); + ASSERT_TRUE(tab); + EXPECT_TRUE( + base::StartsWith(tab->GetVisibleURL().spec(), + GaiaUrls::GetInstance()->add_account_url().spec(), + base::CompareCase::INSENSITIVE_ASCII)); +} + +TEST_F(DiceSigninUiUtilTest, ShowExtensionSigninPrompt_AsLockedProfile) { + signin_util::ScopedForceSigninSetterForTesting force_signin_setter(true); + Profile* profile = browser()->profile(); + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile->GetPath()); + ASSERT_NE(entry, nullptr); + entry->LockForceSigninProfile(true); + TabStripModel* tab_strip = browser()->tab_strip_model(); + ShowExtensionSigninPrompt(profile, /*enable_sync=*/true, + /*email_hint=*/std::string()); + EXPECT_EQ(0, tab_strip->count()); + ShowExtensionSigninPrompt(profile, /*enable_sync=*/false, + /*email_hint=*/std::string()); + EXPECT_EQ(0, tab_strip->count()); +} +#endif // BUILDFLAG(ENABLE_DICE_SUPPORT) + +#if BUILDFLAG(IS_CHROMEOS_LACROS) +class MirrorSigninUiUtilTest : public BrowserWithTestWindowTest { + public: + MirrorSigninUiUtilTest() = default; + ~MirrorSigninUiUtilTest() override = default; + + // BrowserWithTestWindowTest: + TestingProfile::TestingFactories GetTestingFactories() override { + return IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories(); + } +}; + +TEST_F(MirrorSigninUiUtilTest, ShowReauthDialog) { + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile()); + const std::string kEmail = "foo@example.com"; + AccountInfo account_info = signin::MakePrimaryAccountAvailable( + identity_manager, kEmail, signin::ConsentLevel::kSync); + + // Add an account and then put its refresh token into an error state to + // require a reauth before enabling sync. + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + identity_manager, account_info.account_id, + GoogleServiceAuthError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + + account_manager::MockAccountManagerFacade mock_facade; + + EXPECT_CALL(mock_facade, + ShowReauthAccountDialog( + account_manager::AccountManagerFacade::AccountAdditionSource:: + kAvatarBubbleReauthAccountButton, + kEmail)); + internal::ShowReauthForPrimaryAccountWithAuthErrorLacros( + browser(), + signin_metrics::AccessPoint::ACCESS_POINT_AVATAR_BUBBLE_SIGN_IN, + &mock_facade); +} + +TEST_F(MirrorSigninUiUtilTest, ShowExtensionSigninPrompt) { + const std::string kEmail = "foo@example.com"; + TabStripModel* tab_strip = browser()->tab_strip_model(); + account_manager::MockAccountManagerFacade mock_facade; + + EXPECT_CALL( + mock_facade, + ShowReauthAccountDialog(account_manager::AccountManagerFacade:: + AccountAdditionSource::kChromeExtensionReauth, + kEmail)); + internal::ShowExtensionSigninPrompt(browser()->profile(), &mock_facade, + /*enable_sync=*/true, kEmail); + // No tabs should be opened. + EXPECT_EQ(0, tab_strip->count()); +} + +TEST_F(MirrorSigninUiUtilTest, ShowExtensionSigninPrompt_AsLockedProfile) { + signin_util::ScopedForceSigninSetterForTesting force_signin_setter(true); + Profile* profile = browser()->profile(); + ProfileAttributesEntry* entry = + g_browser_process->profile_manager() + ->GetProfileAttributesStorage() + .GetProfileAttributesWithPath(profile->GetPath()); + ASSERT_NE(entry, nullptr); + entry->LockForceSigninProfile(true); + + const std::string kEmail = "foo@example.com"; + TabStripModel* tab_strip = browser()->tab_strip_model(); + account_manager::MockAccountManagerFacade mock_facade; + + EXPECT_CALL(mock_facade, ShowReauthAccountDialog(testing::_, testing::_)) + .Times(0); + internal::ShowExtensionSigninPrompt(browser()->profile(), &mock_facade, + /*enable_sync=*/true, kEmail); + // No dialogs and tabs should be opened. + EXPECT_EQ(0, tab_strip->count()); +} + +#endif // BUILDFLAG(IS_CHROMEOS_LACROS) + +// This test does not use the DiceSigninUiUtilTest test fixture, because it +// needs a mock time environment, and BrowserWithTestWindowTest may be flaky +// when used with mock time (see https://crbug.com/1014790). +TEST(ShouldShowAnimatedIdentityOnOpeningWindow, ReturnsFalseForNewWindow) { + // Setup a testing profile manager with mock time. + content::BrowserTaskEnvironment task_environment( + base::test::TaskEnvironment::TimeSource::MOCK_TIME); + ScopedTestingLocalState local_state(TestingBrowserProcess::GetGlobal()); + TestingProfileManager profile_manager(TestingBrowserProcess::GetGlobal(), + &local_state); + ASSERT_TRUE(profile_manager.SetUp()); + std::string name("testing_profile"); + TestingProfile* profile = profile_manager.CreateTestingProfile( + name, std::unique_ptr<sync_preferences::PrefServiceSyncable>(), + base::UTF8ToUTF16(name), 0, std::string(), + IdentityTestEnvironmentProfileAdaptor:: + GetIdentityTestEnvironmentFactories()); + + // Setup accounts. + signin::IdentityManager* identity_manager = + IdentityManagerFactory::GetForProfile(profile); + identity_manager->GetAccountsMutator()->AddOrUpdateAccount( + kMainGaiaID, kMainEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + identity_manager->GetAccountsMutator()->AddOrUpdateAccount( + kSecondaryGaiaID, kSecondaryEmail, "refresh_token", false, + signin_metrics::SourceForRefreshTokenOperation::kUnknown); + EXPECT_TRUE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager.profile_attributes_storage(), profile)); + + // Animation is shown once. + RecordAnimatedIdentityTriggered(profile); + + // Wait a few seconds. + task_environment.FastForwardBy(base::Seconds(6)); + + // Animation is not shown again in a new window. + EXPECT_FALSE(ShouldShowAnimatedIdentityOnOpeningWindow( + *profile_manager.profile_attributes_storage(), profile)); +} + +} // namespace signin_ui_util diff --git a/chromium/chrome/browser/signin/signin_util.cc b/chromium/chrome/browser/signin/signin_util.cc new file mode 100644 index 00000000000..d3ab11f8671 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util.cc @@ -0,0 +1,382 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/signin_util.h" + +#include <memory> + +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/metrics/histogram_functions.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/supports_user_data.h" +#include "base/task/post_task.h" +#include "base/threading/thread_task_runner_handle.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/policy/cloud/user_policy_signin_service_internal.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profiles_state.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/browser/ui/simple_message_box.h" +#include "chrome/browser/ui/startup/startup_types.h" +#include "chrome/browser/ui/webui/profile_helper.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/url_constants.h" +#include "chrome/grit/generated_resources.h" +#include "components/google/core/common/google_util.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_utils.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "google_apis/gaia/gaia_auth_util.h" +#include "ui/base/l10n/l10n_util.h" + +#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_CHROMEOS) || \ + defined(OS_MAC) +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/browser_list_observer.h" +#include "chrome/browser/ui/browser_window.h" +#define CAN_DELETE_PROFILE +#endif + +namespace signin_util { +namespace { + +constexpr char kSignoutSettingKey[] = "signout_setting"; + +#if defined(CAN_DELETE_PROFILE) +// Manager that presents the profile will be deleted dialog on the first active +// browser window. +class DeleteProfileDialogManager : public BrowserListObserver { + public: + class Delegate { + public: + // Called when the profile was marked for deletion. It is safe for the + // delegate to delete |manager| when this is called. + virtual void OnProfileDeleted(DeleteProfileDialogManager* manager) = 0; + }; + + DeleteProfileDialogManager(std::string primary_account_email, + Delegate* delegate) + : primary_account_email_(primary_account_email), delegate_(delegate) {} + + DeleteProfileDialogManager(const DeleteProfileDialogManager&) = delete; + DeleteProfileDialogManager& operator=(const DeleteProfileDialogManager&) = + delete; + + ~DeleteProfileDialogManager() override { BrowserList::RemoveObserver(this); } + + void PresentDialogOnAllBrowserWindows(Profile* profile) { + DCHECK(profile); + DCHECK(profile_path_.empty()); + profile_path_ = profile->GetPath(); + + BrowserList::AddObserver(this); + Browser* active_browser = chrome::FindLastActiveWithProfile(profile); + if (active_browser) + OnBrowserSetLastActive(active_browser); + } + + void OnBrowserSetLastActive(Browser* browser) override { + DCHECK(!profile_path_.empty()); + + if (profile_path_ != browser->profile()->GetPath()) + return; + + active_browser_ = browser; + + // Display the dialog on the next run loop as otherwise the dialog can block + // browser from displaying because the dialog creates a nested run loop. + // + // This happens because the browser window is not fully created yet when + // OnBrowserSetLastActive() is called. To finish the creation, the code + // needs to return from OnBrowserSetLastActive(). + // + // However, if we open a warning dialog from OnBrowserSetLastActive() + // synchronously, it will create a nested run loop that will not return + // from OnBrowserSetLastActive() until the dialog is dismissed. But the user + // cannot dismiss the dialog because the browser is not even shown! + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&DeleteProfileDialogManager::ShowDeleteProfileDialog, + weak_factory_.GetWeakPtr(), browser)); + } + + // Called immediately after a browser becomes not active. + void OnBrowserNoLongerActive(Browser* browser) override { + if (active_browser_ == browser) + active_browser_ = nullptr; + } + + void OnBrowserRemoved(Browser* browser) override { + if (active_browser_ == browser) + active_browser_ = nullptr; + } + + private: + void ShowDeleteProfileDialog(Browser* browser) { + // Block opening dialog from nested task. + static bool is_dialog_shown = false; + if (is_dialog_shown) + return; + base::AutoReset<bool> auto_reset(&is_dialog_shown, true); + + // Check that |browser| is still active. + if (!active_browser_ || active_browser_ != browser) + return; + + // Show the dialog. + DCHECK(browser->window()->GetNativeWindow()); + chrome::MessageBoxResult result = chrome::ShowWarningMessageBox( + browser->window()->GetNativeWindow(), + l10n_util::GetStringUTF16(IDS_PROFILE_WILL_BE_DELETED_DIALOG_TITLE), + l10n_util::GetStringFUTF16( + IDS_PROFILE_WILL_BE_DELETED_DIALOG_DESCRIPTION, + base::ASCIIToUTF16(primary_account_email_), + base::ASCIIToUTF16( + gaia::ExtractDomainName(primary_account_email_)))); + + switch (result) { + case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_NO: { + // If the warning dialog is automatically dismissed or the user closed + // the dialog by clicking on the close "X" button, then re-present the + // dialog (the user should not be able to interact with the browser + // window as the profile must be deleted). + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&DeleteProfileDialogManager::ShowDeleteProfileDialog, + weak_factory_.GetWeakPtr(), browser)); + break; + } + case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_YES: + webui::DeleteProfileAtPath( + profile_path_, + ProfileMetrics::DELETE_PROFILE_PRIMARY_ACCOUNT_NOT_ALLOWED); + delegate_->OnProfileDeleted(this); + // |this| may be destroyed at this point. Avoid using it. + break; + case chrome::MessageBoxResult::MESSAGE_BOX_RESULT_DEFERRED: + NOTREACHED() << "Message box must not return deferred result when run " + "synchronously"; + break; + } + } + + std::string primary_account_email_; + raw_ptr<Delegate> delegate_; + base::FilePath profile_path_; + raw_ptr<Browser> active_browser_; + base::WeakPtrFactory<DeleteProfileDialogManager> weak_factory_{this}; +}; +#endif // defined(CAN_DELETE_PROFILE) + +// Per-profile manager for the signout allowed setting. +#if defined(CAN_DELETE_PROFILE) +class UserSignoutSetting : public base::SupportsUserData::Data, + public DeleteProfileDialogManager::Delegate { +#else +class UserSignoutSetting : public base::SupportsUserData::Data { +#endif // defined(CAN_DELETE_PROFILE) + public: + enum class State { kUndefined, kAllowed, kDisallowed }; + + // Fetch from Profile. Make and store if not already present. + static UserSignoutSetting* GetForProfile(Profile* profile) { + UserSignoutSetting* signout_setting = static_cast<UserSignoutSetting*>( + profile->GetUserData(kSignoutSettingKey)); + + if (!signout_setting) { + profile->SetUserData(kSignoutSettingKey, + std::make_unique<UserSignoutSetting>()); + signout_setting = static_cast<UserSignoutSetting*>( + profile->GetUserData(kSignoutSettingKey)); + } + + return signout_setting; + } + + State state() const { return state_; } + void set_state(State state) { state_ = state; } + +#if defined(CAN_DELETE_PROFILE) + // Shows the delete profile dialog on the first browser active window. + void ShowDeleteProfileDialog(Profile* profile, const std::string& email) { + if (delete_profile_dialog_manager_) + return; + delete_profile_dialog_manager_ = + std::make_unique<DeleteProfileDialogManager>(email, this); + delete_profile_dialog_manager_->PresentDialogOnAllBrowserWindows(profile); + } + + void OnProfileDeleted(DeleteProfileDialogManager* dialog_manager) override { + DCHECK_EQ(delete_profile_dialog_manager_.get(), dialog_manager); + delete_profile_dialog_manager_.reset(); + } +#endif + + private: + State state_ = State::kUndefined; + +#if defined(CAN_DELETE_PROFILE) + std::unique_ptr<DeleteProfileDialogManager> delete_profile_dialog_manager_; +#endif +}; + +enum ForceSigninPolicyCache { + NOT_CACHED = 0, + ENABLE, + DISABLE +} g_is_force_signin_enabled_cache = NOT_CACHED; + +void SetForceSigninPolicy(bool enable) { + g_is_force_signin_enabled_cache = enable ? ENABLE : DISABLE; +} + +} // namespace + +ScopedForceSigninSetterForTesting::ScopedForceSigninSetterForTesting( + bool enable) { + SetForceSigninForTesting(enable); // IN-TEST +} + +ScopedForceSigninSetterForTesting::~ScopedForceSigninSetterForTesting() { + ResetForceSigninForTesting(); // IN-TEST +} + +bool IsForceSigninEnabled() { + if (g_is_force_signin_enabled_cache == NOT_CACHED) { + PrefService* prefs = g_browser_process->local_state(); + if (prefs) + SetForceSigninPolicy(prefs->GetBoolean(prefs::kForceBrowserSignin)); + else + return false; + } + return (g_is_force_signin_enabled_cache == ENABLE); +} + +void SetForceSigninForTesting(bool enable) { + SetForceSigninPolicy(enable); +} + +void ResetForceSigninForTesting() { + g_is_force_signin_enabled_cache = NOT_CACHED; +} + +bool IsUserSignoutAllowedForProfile(Profile* profile) { + return UserSignoutSetting::GetForProfile(profile)->state() == + UserSignoutSetting::State::kAllowed; +} + +void EnsureUserSignoutAllowedIsInitializedForProfile(Profile* profile) { + if (UserSignoutSetting::GetForProfile(profile)->state() == + UserSignoutSetting::State::kUndefined) { + SetUserSignoutAllowedForProfile(profile, true); + } +} + +void SetUserSignoutAllowedForProfile(Profile* profile, bool is_allowed) { + UserSignoutSetting::State new_state = + is_allowed ? UserSignoutSetting::State::kAllowed + : UserSignoutSetting::State::kDisallowed; + UserSignoutSetting::GetForProfile(profile)->set_state(new_state); +} + +void EnsurePrimaryAccountAllowedForProfile(Profile* profile) { +// All primary accounts are allowed on ChromeOS, so this method is a no-op on +// ChromeOS. +#if !BUILDFLAG(IS_CHROMEOS_ASH) + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + if (!identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) + return; + + CoreAccountInfo primary_account = + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync); + if (profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed) && + signin::IsUsernameAllowedByPatternFromPrefs( + g_browser_process->local_state(), primary_account.email)) { + return; + } + + UserSignoutSetting* signout_setting = + UserSignoutSetting::GetForProfile(profile); + switch (signout_setting->state()) { + case UserSignoutSetting::State::kUndefined: + NOTREACHED(); + break; + case UserSignoutSetting::State::kAllowed: { + // Force clear the primary account if it is no longer allowed and if sign + // out is allowed. + auto* primary_account_mutator = + identity_manager->GetPrimaryAccountMutator(); + primary_account_mutator->ClearPrimaryAccount( + signin_metrics::SIGNIN_NOT_ALLOWED_ON_PROFILE_INIT, + signin_metrics::SignoutDelete::kIgnoreMetric); + break; + } + case UserSignoutSetting::State::kDisallowed: +#if defined(CAN_DELETE_PROFILE) + // Force remove the profile if sign out is not allowed and if the + // primary account is no longer allowed. + // This may be called while the profile is initializing, so it must be + // scheduled for later to allow the profile initialization to complete. + CHECK(profiles::IsMultipleProfilesEnabled()); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&UserSignoutSetting::ShowDeleteProfileDialog, + base::Unretained(signout_setting), profile, + primary_account.email)); +#else + CHECK(false) << "Deleting profiles is not supported."; +#endif // defined(CAN_DELETE_PROFILE) + break; + } +#endif // !BUILDFLAG(IS_CHROMEOS_ASH) +} + +#if !defined(OS_ANDROID) +bool ProfileSeparationEnforcedByPolicy( + Profile* profile, + const std::string& intercepted_account_level_policy_value) { + if (!base::FeatureList::IsEnabled(kAccountPoliciesLoadedWithoutSync)) + return false; + std::string current_profile_account_restriction = + profile->GetPrefs()->GetString(prefs::kManagedAccountsSigninRestriction); + + bool is_machine_level_policy = profile->GetPrefs()->GetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine); + + // Enforce profile separation for all new signins if any restriction is + // applied at a machine level. + if (is_machine_level_policy) { + return !current_profile_account_restriction.empty() && + current_profile_account_restriction != "none"; + } + + // Enforce profile separation for all new signins if "primary_account_strict" + // is set at the user account level. + return current_profile_account_restriction == "primary_account_strict" || + base::StartsWith(intercepted_account_level_policy_value, + "primary_account"); +} + +void RecordEnterpriseProfileCreationUserChoice(bool enforced_by_policy, + bool created) { + base::UmaHistogramBoolean( + enforced_by_policy + ? "Signin.Enterprise.WorkProfile.ProfileCreatedWithPolicySet" + : "Signin.Enterprise.WorkProfile.ProfileCreatedwithPolicyUnset", + created); +} + +#endif + +} // namespace signin_util diff --git a/chromium/chrome/browser/signin/signin_util.h b/chromium/chrome/browser/signin/signin_util.h new file mode 100644 index 00000000000..10a436d9ea1 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util.h @@ -0,0 +1,73 @@ +// Copyright 2017 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_ + +#include <string> + +#include "build/build_config.h" + +class Profile; + +namespace signin_util { + +// This class calls ResetForceSigninForTesting when destroyed, so that +// ForcedSigning doesn't leak across tests. +class ScopedForceSigninSetterForTesting { + public: + explicit ScopedForceSigninSetterForTesting(bool enable); + ~ScopedForceSigninSetterForTesting(); +}; + +// Return whether the force sign in policy is enabled or not. +// The state of this policy will not be changed without relaunch Chrome. +bool IsForceSigninEnabled(); + +// Enable or disable force sign in for testing. Please use +// ScopedForceSigninSetterForTesting instead, if possible. If not, make sure +// ResetForceSigninForTesting is called before the test finishes. +void SetForceSigninForTesting(bool enable); + +// Reset force sign in to uninitialized state for testing. +void ResetForceSigninForTesting(); + +// Returns true if clearing the primary profile is allowed. +bool IsUserSignoutAllowedForProfile(Profile* profile); + +// Sign-out is allowed by default, but some Chrome profiles (e.g. for cloud- +// managed enterprise accounts) may wish to disallow user-initiated sign-out. +// Note that this exempts sign-outs that are not user-initiated (e.g. sign-out +// triggered when cloud policy no longer allows current email pattern). See +// ChromeSigninClient::PreSignOut(). +void SetUserSignoutAllowedForProfile(Profile* profile, bool is_allowed); + +// Updates the user sign-out state to |true| if is was never initialized. +// This should be called at the end of the flow to initialize a profile to +// ensure that the signout allowed flag is updated. +void EnsureUserSignoutAllowedIsInitializedForProfile(Profile* profile); + +// Ensures that the primary account for |profile| is allowed: +// * If profile does not have any primary account, then this is a no-op. +// * If |IsUserSignoutAllowedForProfile| is allowed and the primary account +// is no longer allowed, then this clears the primary account. +// * If |IsUserSignoutAllowedForProfile| is not allowed and the primary account +// is not longer allowed, then this removes the profile. +void EnsurePrimaryAccountAllowedForProfile(Profile* profile); + +#if !defined(OS_ANDROID) +// Returns true if profile separation is enforced by policy. +bool ProfileSeparationEnforcedByPolicy( + Profile* profile, + const std::string& intercepted_account_level_policy_value); + +// Records a UMA metric if the user accepts or not to create an enterprise +// profile. +void RecordEnterpriseProfileCreationUserChoice(bool enforced_by_policy, + bool created); +#endif + +} // namespace signin_util + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_H_ diff --git a/chromium/chrome/browser/signin/signin_util_unittest.cc b/chromium/chrome/browser/signin/signin_util_unittest.cc new file mode 100644 index 00000000000..80862578339 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util_unittest.cc @@ -0,0 +1,127 @@ +// Copyright 2017 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. + +#include "chrome/browser/signin/signin_util.h" + +#include <memory> + +#include "base/feature_list.h" +#include "build/buildflag.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/prefs/browser_prefs.h" +#include "chrome/browser/signin/signin_features.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/browser_with_test_window_test.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "components/prefs/pref_service.h" +#include "components/prefs/testing_pref_service.h" + +class SigninUtilTest : public BrowserWithTestWindowTest { + public: + void SetUp() override { + BrowserWithTestWindowTest::SetUp(); + signin_util::ResetForceSigninForTesting(); + } + + void TearDown() override { + signin_util::ResetForceSigninForTesting(); + BrowserWithTestWindowTest::TearDown(); + } +}; + +TEST_F(SigninUtilTest, GetForceSigninPolicy) { + EXPECT_FALSE(signin_util::IsForceSigninEnabled()); + + g_browser_process->local_state()->SetBoolean(prefs::kForceBrowserSignin, + true); + signin_util::ResetForceSigninForTesting(); + EXPECT_TRUE(signin_util::IsForceSigninEnabled()); + g_browser_process->local_state()->SetBoolean(prefs::kForceBrowserSignin, + false); + signin_util::ResetForceSigninForTesting(); + EXPECT_FALSE(signin_util::IsForceSigninEnabled()); +} + +#if !BUILDFLAG(IS_CHROMEOS_LACROS) +class SigninUtilEnterpriseTest : public BrowserWithTestWindowTest { + public: + SigninUtilEnterpriseTest() + : feature_list_(kAccountPoliciesLoadedWithoutSync) {} + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(SigninUtilEnterpriseTest, ProfileSeparationEnforcedByPolicy) { + std::unique_ptr<TestingProfile> profile = TestingProfile::Builder().Build(); + + // No policy set on the active profile. + EXPECT_FALSE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_FALSE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); + + // Active profile has "primary_account" as a user level policy. + profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + profile->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, false); + EXPECT_FALSE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_FALSE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); + + // Active profile has "primary_account_strict" as a user level policy. + profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account_strict"); + profile->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, false); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_TRUE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); + + // Active profile has "primary_account" as a machine level policy. + profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + profile->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_TRUE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); + + // Active profile has "primary_account_strict" as a machine level policy. + profile->GetPrefs()->SetString(prefs::kManagedAccountsSigninRestriction, + "primary_account"); + profile->GetPrefs()->SetBoolean( + prefs::kManagedAccountsSigninRestrictionScopeMachine, true); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), + std::string())); + EXPECT_TRUE( + signin_util::ProfileSeparationEnforcedByPolicy(profile.get(), "none")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account")); + EXPECT_TRUE(signin_util::ProfileSeparationEnforcedByPolicy( + profile.get(), "primary_account_strict")); +} +#endif diff --git a/chromium/chrome/browser/signin/signin_util_win.cc b/chromium/chrome/browser/signin/signin_util_win.cc new file mode 100644 index 00000000000..0ffc61db1b6 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util_win.cc @@ -0,0 +1,332 @@ +// Copyright 2018 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. + +#include "chrome/browser/signin/signin_util_win.h" + +#include <memory> +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/no_destructor.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/win/registry.h" +#include "base/win/win_util.h" +#include "base/win/wincrypt_shim.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/first_run/first_run.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_window.h" +#include "chrome/browser/signin/about_signin_internals_factory.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#include "chrome/browser/ui/webui/signin/signin_ui_error.h" +#include "chrome/browser/ui/webui/signin/signin_utils_desktop.h" +#include "chrome/credential_provider/common/gcp_strings.h" +#include "components/prefs/pref_service.h" +#include "components/signin/core/browser/about_signin_internals.h" +#include "components/signin/public/base/signin_metrics.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/accounts_mutator.h" +#include "components/signin/public/identity_manager/identity_manager.h" + +namespace signin_util { + +namespace { + +std::unique_ptr<DiceTurnSyncOnHelper::Delegate>* +GetDiceTurnSyncOnHelperDelegateForTestingStorage() { + static base::NoDestructor<std::unique_ptr<DiceTurnSyncOnHelper::Delegate>> + delegate; + return delegate.get(); +} + +std::string DecryptRefreshToken(const std::string& cipher_text) { + DATA_BLOB input; + input.pbData = + const_cast<BYTE*>(reinterpret_cast<const BYTE*>(cipher_text.data())); + input.cbData = static_cast<DWORD>(cipher_text.length()); + DATA_BLOB output; + BOOL result = ::CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &output); + + if (!result) + return std::string(); + + std::string refresh_token(reinterpret_cast<char*>(output.pbData), + output.cbData); + ::LocalFree(output.pbData); + return refresh_token; +} + +// Finish the process of import credentials. This is either called directly +// from ImportCredentialsFromProvider() if a browser window for the profile is +// already available or is delayed until a browser can first be opened. +void FinishImportCredentialsFromProvider(const CoreAccountId& account_id, + Browser* browser, + Profile* profile, + Profile::CreateStatus status) { + // DiceTurnSyncOnHelper deletes itself once done. + if (GetDiceTurnSyncOnHelperDelegateForTestingStorage()->get()) { + new DiceTurnSyncOnHelper( + profile, signin_metrics::AccessPoint::ACCESS_POINT_MACHINE_LOGON, + signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT, + signin_metrics::Reason::kSigninPrimaryAccount, account_id, + DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT, + std::move(*GetDiceTurnSyncOnHelperDelegateForTestingStorage()), + base::DoNothing()); + } else { + if (!browser) + browser = chrome::FindLastActiveWithProfile(profile); + + new DiceTurnSyncOnHelper( + profile, browser, + signin_metrics::AccessPoint::ACCESS_POINT_MACHINE_LOGON, + signin_metrics::PromoAction::PROMO_ACTION_WITH_DEFAULT, + signin_metrics::Reason::kSigninPrimaryAccount, account_id, + DiceTurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT); + } +} + +// Start the process of importing credentials from the credential provider given +// that all the required information is available. The process depends on +// having a browser window for the profile. If a browser window exists the +// profile be signed in and sync will be starting up. If not, the profile will +// be still be signed in but sync will be started once the browser window is +// ready. +void ImportCredentialsFromProvider(Profile* profile, + const std::wstring& gaia_id, + const std::wstring& email, + const std::string& refresh_token, + bool turn_on_sync) { + // For debugging purposes, record that the credentials for this profile + // came from a credential provider. + AboutSigninInternals* signin_internals = + AboutSigninInternalsFactory::GetInstance()->GetForProfile(profile); + signin_internals->OnAuthenticationResultReceived("Credential Provider"); + + CoreAccountId account_id = + IdentityManagerFactory::GetForProfile(profile) + ->GetAccountsMutator() + ->AddOrUpdateAccount(base::WideToUTF8(gaia_id), + base::WideToUTF8(email), refresh_token, + /*is_under_advanced_protection=*/false, + signin_metrics::SourceForRefreshTokenOperation:: + kMachineLogon_CredentialProvider); + + if (turn_on_sync) { + Browser* browser = chrome::FindLastActiveWithProfile(profile); + if (browser) { + FinishImportCredentialsFromProvider(account_id, browser, profile, + Profile::CREATE_STATUS_CREATED); + } else { + // If no active browser exists yet, this profile is in the process of + // being created. Wait for the browser to be created before finishing the + // sign in. This object deletes itself when done. + new profiles::BrowserAddedForProfileObserver( + profile, base::BindRepeating(&FinishImportCredentialsFromProvider, + account_id, nullptr)); + } + } + + // Mark this profile as having been signed in with the credential provider. + profile->GetPrefs()->SetBoolean(prefs::kSignedInWithCredentialProvider, true); +} + +// Extracts the |cred_provider_gaia_id| and |cred_provider_email| for the user +// signed in throuhg credential provider. +void ExtractCredentialProviderUser(std::wstring* cred_provider_gaia_id, + std::wstring* cred_provider_email) { + DCHECK(cred_provider_gaia_id); + DCHECK(cred_provider_email); + + cred_provider_gaia_id->clear(); + cred_provider_email->clear(); + + base::win::RegKey key; + if (key.Open(HKEY_CURRENT_USER, credential_provider::kRegHkcuAccountsPath, + KEY_READ) != ERROR_SUCCESS) { + return; + } + + base::win::RegistryKeyIterator it(key.Handle(), L""); + if (!it.Valid() || it.SubkeyCount() != 1) + return; + + base::win::RegKey key_account(key.Handle(), it.Name(), KEY_QUERY_VALUE); + if (!key_account.Valid()) + return; + + std::wstring email; + if (key_account.ReadValue( + base::UTF8ToWide(credential_provider::kKeyEmail).c_str(), &email) != + ERROR_SUCCESS) { + return; + } + + *cred_provider_gaia_id = it.Name(); + *cred_provider_email = email; +} + +// Attempt to sign in with a credentials from a system installed credential +// provider if available. If |auth_gaia_id| is not empty then the system +// credential must be for the same account. Starts the process to turn on DICE +// only if |turn_on_sync| is true. +bool TrySigninWithCredentialProvider(Profile* profile, + const std::wstring& auth_gaia_id, + bool turn_on_sync) { + base::win::RegKey key; + if (key.Open(HKEY_CURRENT_USER, credential_provider::kRegHkcuAccountsPath, + KEY_READ) != ERROR_SUCCESS) { + return false; + } + + base::win::RegistryKeyIterator it(key.Handle(), L""); + if (!it.Valid() || it.SubkeyCount() == 0) + return false; + + base::win::RegKey key_account(key.Handle(), it.Name(), KEY_READ | KEY_WRITE); + if (!key_account.Valid()) + return false; + + std::wstring gaia_id = it.Name(); + if (!auth_gaia_id.empty() && auth_gaia_id != gaia_id) + return false; + + std::wstring email; + if (key_account.ReadValue( + base::UTF8ToWide(credential_provider::kKeyEmail).c_str(), &email) != + ERROR_SUCCESS) { + return false; + } + + // Read the encrypted refresh token. The data is stored in binary format. + // No matter what happens, delete the registry entry. + + std::string encrypted_refresh_token; + DWORD size = 0; + DWORD type; + if (key_account.ReadValue( + base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str(), + nullptr, &size, &type) != ERROR_SUCCESS) { + return false; + } + + encrypted_refresh_token.resize(size); + bool reauth_attempted = false; + key_account.ReadValue( + base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str(), + const_cast<char*>(encrypted_refresh_token.c_str()), &size, &type); + if (!gaia_id.empty() && !email.empty() && type == REG_BINARY && + !encrypted_refresh_token.empty()) { + std::string refresh_token = DecryptRefreshToken(encrypted_refresh_token); + if (!refresh_token.empty()) { + reauth_attempted = true; + ImportCredentialsFromProvider(profile, gaia_id, email, refresh_token, + turn_on_sync); + } + } + + key_account.DeleteValue( + base::UTF8ToWide(credential_provider::kKeyRefreshToken).c_str()); + return reauth_attempted; +} + +} // namespace + +void SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr<DiceTurnSyncOnHelper::Delegate> delegate) { + GetDiceTurnSyncOnHelperDelegateForTestingStorage()->swap(delegate); +} + +// Credential provider needs to stick to profile it previously used to import +// credentials. Thus, if there is another profile that was previously signed in +// with credential provider regardless of whether user signed in or out, +// credential provider shouldn't attempt to import credentials into current +// profile. +bool IsGCPWUsedInOtherProfile(Profile* profile) { + DCHECK(profile); + + ProfileManager* profile_manager = g_browser_process->profile_manager(); + if (profile_manager) { + std::vector<ProfileAttributesEntry*> entries = + profile_manager->GetProfileAttributesStorage() + .GetAllProfilesAttributes(); + + for (const ProfileAttributesEntry* entry : entries) { + if (entry->GetPath() == profile->GetPath()) + continue; + + if (entry->IsSignedInWithCredentialProvider()) + return true; + } + } + + return false; +} + +void SigninWithCredentialProviderIfPossible(Profile* profile) { + // This flow is used for first time signin through credential provider. Any + // subsequent signin for the credential provider user needs to go through + // reauth flow. + if (profile->GetPrefs()->GetBoolean(prefs::kSignedInWithCredentialProvider)) + return; + + std::wstring cred_provider_gaia_id; + std::wstring cred_provider_email; + + ExtractCredentialProviderUser(&cred_provider_gaia_id, &cred_provider_email); + if (cred_provider_gaia_id.empty() || cred_provider_email.empty()) + return; + + // Chrome doesn't allow signing into current profile if the same user is + // signed in another profile. + if (!CanOfferSignin(profile, base::WideToUTF8(cred_provider_gaia_id), + base::WideToUTF8(cred_provider_email)) + .IsOk() || + IsGCPWUsedInOtherProfile(profile)) { + return; + } + + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + std::wstring gaia_id; + if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)) { + gaia_id = base::UTF8ToWide( + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync) + .gaia); + } + + TrySigninWithCredentialProvider(profile, gaia_id, gaia_id.empty()); +} + +bool ReauthWithCredentialProviderIfPossible(Profile* profile) { + // Check to see if auto signin information is available. Only applies if: + // + // - The profile is marked as having been signed in with a system credential. + // - The profile is already signed in. + // - The profile is in an auth error state. + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + if (!(profile->GetPrefs()->GetBoolean( + prefs::kSignedInWithCredentialProvider) && + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) && + identity_manager->HasAccountWithRefreshTokenInPersistentErrorState( + identity_manager->GetPrimaryAccountId( + signin::ConsentLevel::kSync)))) { + return false; + } + + std::wstring gaia_id = base::UTF8ToWide( + identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync) + .gaia.c_str()); + return TrySigninWithCredentialProvider(profile, gaia_id, false); +} + +} // namespace signin_util diff --git a/chromium/chrome/browser/signin/signin_util_win.h b/chromium/chrome/browser/signin/signin_util_win.h new file mode 100644 index 00000000000..771085f9bbb --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util_win.h @@ -0,0 +1,31 @@ +// Copyright 2018 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. + +#ifndef CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_ +#define CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_ + +#include <memory> + +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" + +class Profile; + +namespace signin_util { + +// Attempt to sign in with a credentials from a system installed credential +// provider if available. +void SigninWithCredentialProviderIfPossible(Profile* profile); + +// Attempt to reauthenticate with a credentials from a system installed +// credential provider if available. If a new authentication token was +// installed returns true. +bool ReauthWithCredentialProviderIfPossible(Profile* profile); + +// Sets the DiceTurnSyncOnHelper delegate for browser tests. +void SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr<DiceTurnSyncOnHelper::Delegate> delegate); + +} // namespace signin_util + +#endif // CHROME_BROWSER_SIGNIN_SIGNIN_UTIL_WIN_H_ diff --git a/chromium/chrome/browser/signin/signin_util_win_browsertest.cc b/chromium/chrome/browser/signin/signin_util_win_browsertest.cc new file mode 100644 index 00000000000..87832ab6e98 --- /dev/null +++ b/chromium/chrome/browser/signin/signin_util_win_browsertest.cc @@ -0,0 +1,698 @@ +// Copyright 2018 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. + +#include <stddef.h> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/command_line.h" +#include "base/run_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/bind.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/test_reg_util_win.h" +#include "base/win/wincrypt_shim.h" +#include "build/build_config.h" +#include "chrome/browser/first_run/first_run.h" +#include "chrome/browser/profiles/profile_attributes_entry.h" +#include "chrome/browser/profiles/profile_attributes_storage.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/profiles/profile_window.h" +#include "chrome/browser/signin/identity_manager_factory.h" +#include "chrome/browser/signin/signin_util_win.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/browser/ui/webui/signin/dice_turn_sync_on_helper.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "chrome/credential_provider/common/gcp_strings.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/testing_browser_process.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/base/signin_pref_names.h" +#include "components/signin/public/identity_manager/identity_manager.h" +#include "components/signin/public/identity_manager/identity_test_utils.h" +#include "components/signin/public/identity_manager/primary_account_mutator.h" +#include "content/public/test/browser_test.h" + +class SigninUIError; + +namespace { + +class TestDiceTurnSyncOnHelperDelegate : public DiceTurnSyncOnHelper::Delegate { + ~TestDiceTurnSyncOnHelperDelegate() override {} + + // DiceTurnSyncOnHelper::Delegate: + void ShowLoginError(const SigninUIError& error) override {} + void ShowMergeSyncDataConfirmation( + const std::string& previous_email, + const std::string& new_email, + DiceTurnSyncOnHelper::SigninChoiceCallback callback) override { + std::move(callback).Run(DiceTurnSyncOnHelper::SIGNIN_CHOICE_CONTINUE); + } + void ShowEnterpriseAccountConfirmation( + const AccountInfo& account_info, + DiceTurnSyncOnHelper::SigninChoiceCallback callback) override { + std::move(callback).Run(DiceTurnSyncOnHelper::SIGNIN_CHOICE_CONTINUE); + } + void ShowSyncConfirmation( + base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)> + callback) override { + std::move(callback).Run(LoginUIService::SYNC_WITH_DEFAULT_SETTINGS); + } + void ShowSyncDisabledConfirmation( + bool is_managed_account, + base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)> + callback) override {} + void ShowSyncSettings() override {} + void SwitchToProfile(Profile* new_profile) override {} +}; + +struct SigninUtilWinBrowserTestParams { + SigninUtilWinBrowserTestParams(bool is_first_run, + const std::wstring& gaia_id, + const std::wstring& email, + const std::string& refresh_token, + bool expect_is_started) + : is_first_run(is_first_run), + gaia_id(gaia_id), + email(email), + refresh_token(refresh_token), + expect_is_started(expect_is_started) {} + + bool is_first_run = false; + std::wstring gaia_id; + std::wstring email; + std::string refresh_token; + bool expect_is_started = false; +}; + +void AssertSigninStarted(bool expect_is_started, Profile* profile) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + + ProfileAttributesStorage& storage = + profile_manager->GetProfileAttributesStorage(); + + ProfileAttributesEntry* entry = + storage.GetProfileAttributesWithPath(profile->GetPath()); + + ASSERT_NE(entry, nullptr); + + ASSERT_EQ(expect_is_started, entry->IsSignedInWithCredentialProvider()); +} + +} // namespace + +class BrowserTestHelper { + public: + BrowserTestHelper(const std::wstring& gaia_id, + const std::wstring& email, + const std::string& refresh_token) + : gaia_id_(gaia_id), email_(email), refresh_token_(refresh_token) {} + + protected: + void CreateRegKey(base::win::RegKey* key) { + if (!gaia_id_.empty()) { + EXPECT_EQ( + ERROR_SUCCESS, + key->Create(HKEY_CURRENT_USER, + credential_provider::kRegHkcuAccountsPath, KEY_WRITE)); + EXPECT_EQ(ERROR_SUCCESS, key->CreateKey(gaia_id_.c_str(), KEY_WRITE)); + } + } + + void WriteRefreshToken(base::win::RegKey* key, + const std::string& refresh_token) { + EXPECT_TRUE(key->Valid()); + DATA_BLOB plaintext; + plaintext.pbData = + reinterpret_cast<BYTE*>(const_cast<char*>(refresh_token.c_str())); + plaintext.cbData = static_cast<DWORD>(refresh_token.length()); + + DATA_BLOB ciphertext; + ASSERT_TRUE(::CryptProtectData(&plaintext, L"Gaia refresh token", nullptr, + nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, + &ciphertext)); + std::string encrypted_data(reinterpret_cast<char*>(ciphertext.pbData), + ciphertext.cbData); + EXPECT_EQ( + ERROR_SUCCESS, + key->WriteValue( + base::ASCIIToWide(credential_provider::kKeyRefreshToken).c_str(), + encrypted_data.c_str(), encrypted_data.length(), REG_BINARY)); + LocalFree(ciphertext.pbData); + } + + void ExpectRefreshTokenExists(bool exists) { + base::win::RegKey key; + EXPECT_EQ(ERROR_SUCCESS, + key.Open(HKEY_CURRENT_USER, + credential_provider::kRegHkcuAccountsPath, KEY_READ)); + EXPECT_EQ(ERROR_SUCCESS, key.OpenKey(gaia_id_.c_str(), KEY_READ)); + EXPECT_EQ( + exists, + key.HasValue( + base::ASCIIToWide(credential_provider::kKeyRefreshToken).c_str())); + } + + public: + void SetSigninUtilRegistry() { + base::win::RegKey key; + CreateRegKey(&key); + + if (!email_.empty()) { + EXPECT_TRUE(key.Valid()); + EXPECT_EQ(ERROR_SUCCESS, + key.WriteValue( + base::ASCIIToWide(credential_provider::kKeyEmail).c_str(), + email_.c_str())); + } + + if (!refresh_token_.empty()) + WriteRefreshToken(&key, refresh_token_); + } + + bool IsPreTest() { + std::string test_name = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + LOG(INFO) << "PRE_ test_name " << test_name; + return test_name.find("PRE_") != std::string::npos; + } + + bool IsPrePreTest() { + std::string test_name = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + LOG(INFO) << "PRE_PRE_ test_name " << test_name; + return test_name.find("PRE_PRE_") != std::string::npos; + } + + private: + std::wstring gaia_id_; + std::wstring email_; + std::string refresh_token_; +}; + +class SigninUtilWinBrowserTest + : public BrowserTestHelper, + public InProcessBrowserTest, + public testing::WithParamInterface<SigninUtilWinBrowserTestParams> { + public: + SigninUtilWinBrowserTest() + : BrowserTestHelper(GetParam().gaia_id, + GetParam().email, + GetParam().refresh_token) {} + + protected: + void SetUpCommandLine(base::CommandLine* command_line) override { + command_line->AppendSwitch(GetParam().is_first_run + ? switches::kForceFirstRun + : switches::kNoFirstRun); + } + + bool SetUpUserDataDirectory() override { + registry_override_.OverrideRegistry(HKEY_CURRENT_USER); + + signin_util::SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr<DiceTurnSyncOnHelper::Delegate>( + new TestDiceTurnSyncOnHelperDelegate())); + + SetSigninUtilRegistry(); + + return InProcessBrowserTest::SetUpUserDataDirectory(); + } + + private: + registry_util::RegistryOverrideManager registry_override_; +}; + +IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, Run) { + ASSERT_EQ(GetParam().is_first_run, first_run::IsChromeFirstRun()); + + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName()); + + Browser* browser = chrome::FindLastActiveWithProfile(profile); + ASSERT_NE(nullptr, browser); + + AssertSigninStarted(GetParam().expect_is_started, profile); + + // If a refresh token was specified and a sign in attempt was expected, make + // sure the refresh token was removed from the registry. + if (!GetParam().refresh_token.empty() && GetParam().expect_is_started) + ExpectRefreshTokenExists(false); +} + +IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, ReauthNoop) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + + // Whether the profile was signed in with the credential provider or not, + // reauth should be a noop. + ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); +} + +IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, NoReauthAfterSignout) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + + if (GetParam().expect_is_started) { + // Write a new refresh token. + base::win::RegKey key; + CreateRegKey(&key); + WriteRefreshToken(&key, "lst-new"); + ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); + + // Sign user out of browser. + auto* primary_account_mutator = + IdentityManagerFactory::GetForProfile(profile) + ->GetPrimaryAccountMutator(); + primary_account_mutator->RevokeSyncConsent( + signin_metrics::FORCE_SIGNOUT_ALWAYS_ALLOWED_FOR_TEST, + signin_metrics::SignoutDelete::kDeleted); + + // Even with a refresh token available, no reauth happens if the profile + // is signed out. + ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); + } +} + +IN_PROC_BROWSER_TEST_P(SigninUtilWinBrowserTest, FixReauth) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + + if (GetParam().expect_is_started) { + // Write a new refresh token. This time reauth should work. + base::win::RegKey key; + CreateRegKey(&key); + WriteRefreshToken(&key, "lst-new"); + ASSERT_FALSE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); + + // Make sure the profile stays signed in, but in an auth error state. + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + signin::UpdatePersistentErrorOfRefreshTokenForAccount( + identity_manager, + identity_manager->GetPrimaryAccountId(signin::ConsentLevel::kSync), + GoogleServiceAuthError::FromInvalidGaiaCredentialsReason( + GoogleServiceAuthError::InvalidGaiaCredentialsReason:: + CREDENTIALS_REJECTED_BY_SERVER)); + + // If the profile remains signed in but is in an auth error state, + // reauth should happen. + ASSERT_TRUE(signin_util::ReauthWithCredentialProviderIfPossible(profile)); + } +} + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest1, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/false, + /*gaia_id=*/std::wstring(), + /*email=*/std::wstring(), + /*refresh_token=*/std::string(), + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest2, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/true, + /*gaia_id=*/std::wstring(), + /*email=*/std::wstring(), + /*refresh_token=*/std::string(), + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest3, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/true, + /*gaia_id=*/L"gaia-123456", + /*email=*/std::wstring(), + /*refresh_token=*/std::string(), + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest4, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/true, + /*gaia_id=*/L"gaia-123456", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/std::string(), + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest5, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/true, + /*gaia_id=*/L"gaia-123456", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P(SigninUtilWinBrowserTest6, + SigninUtilWinBrowserTest, + testing::Values(SigninUtilWinBrowserTestParams( + /*is_first_run=*/false, + /*gaia_id=*/L"gaia-123456", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*expect_is_started=*/true))); + +struct ExistingWinBrowserSigninUtilTestParams : SigninUtilWinBrowserTestParams { + ExistingWinBrowserSigninUtilTestParams( + const std::wstring& gaia_id, + const std::wstring& email, + const std::string& refresh_token, + const std::wstring& existing_email, + bool expect_is_started) + : SigninUtilWinBrowserTestParams(false, + gaia_id, + email, + refresh_token, + expect_is_started), + existing_email(existing_email) {} + + std::wstring existing_email; +}; + +class ExistingWinBrowserSigninUtilTest + : public BrowserTestHelper, + public InProcessBrowserTest, + public testing::WithParamInterface< + ExistingWinBrowserSigninUtilTestParams> { + public: + ExistingWinBrowserSigninUtilTest() + : BrowserTestHelper(GetParam().gaia_id, + GetParam().email, + GetParam().refresh_token) {} + + protected: + bool SetUpUserDataDirectory() override { + registry_override_.OverrideRegistry(HKEY_CURRENT_USER); + + signin_util::SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr<DiceTurnSyncOnHelper::Delegate>( + new TestDiceTurnSyncOnHelperDelegate())); + if (!IsPreTest()) + SetSigninUtilRegistry(); + + return InProcessBrowserTest::SetUpUserDataDirectory(); + } + + private: + registry_util::RegistryOverrideManager registry_override_; +}; + +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserSigninUtilTest, + PRE_ExistingWinBrowser) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + Profile* profile = profile_manager->GetLastUsedProfile(); + + ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName()); + + if (!GetParam().existing_email.empty()) { + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + + ASSERT_TRUE(identity_manager); + + signin::MakePrimaryAccountAvailable( + identity_manager, base::WideToUTF8(GetParam().existing_email), + signin::ConsentLevel::kSync); + + ASSERT_TRUE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)); + } +} + +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserSigninUtilTest, ExistingWinBrowser) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_EQ(1u, profile_manager->GetNumberOfProfiles()); + + Profile* profile = profile_manager->GetLastUsedProfile(); + ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName()); + + AssertSigninStarted(GetParam().expect_is_started, profile); + + // If a refresh token was specified and a sign in attempt was expected, make + // sure the refresh token was removed from the registry. + if (!GetParam().refresh_token.empty() && GetParam().expect_is_started) + ExpectRefreshTokenExists(false); +} + +INSTANTIATE_TEST_SUITE_P(AllowSubsequentRun, + ExistingWinBrowserSigninUtilTest, + testing::Values(ExistingWinBrowserSigninUtilTestParams( + /*gaia_id=*/L"gaia-123456", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*existing_email=*/std::wstring(), + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P(OnlyAllowProfileWithNoPrimaryAccount, + ExistingWinBrowserSigninUtilTest, + testing::Values(ExistingWinBrowserSigninUtilTestParams( + /*gaia_id=*/L"gaia_id_for_foo_gmail.com", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*existing_email=*/L"bar@gmail.com", + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P(AllowProfileWithPrimaryAccount_DifferentUser, + ExistingWinBrowserSigninUtilTest, + testing::Values(ExistingWinBrowserSigninUtilTestParams( + /*gaia_id=*/L"gaia_id_for_foo_gmail.com", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*existing_email=*/L"bar@gmail.com", + /*expect_is_started=*/false))); + + +INSTANTIATE_TEST_SUITE_P(AllowProfileWithPrimaryAccount_SameUser, + ExistingWinBrowserSigninUtilTest, + testing::Values(ExistingWinBrowserSigninUtilTestParams( + /*gaia_id=*/L"gaia_id_for_foo_gmail.com", + /*email=*/L"foo@gmail.com", + /*refresh_token=*/"lst-123456", + /*existing_email=*/L"foo@gmail.com", + /*expect_is_started=*/true))); + +void UnblockOnProfileInitialized(base::OnceClosure quit_closure, + Profile* profile, + Profile::CreateStatus status) { + // If the status is CREATE_STATUS_CREATED, then the function will be called + // again with CREATE_STATUS_INITIALIZED. + if (status == Profile::CREATE_STATUS_CREATED) + return; + + EXPECT_EQ(Profile::CREATE_STATUS_INITIALIZED, status); + std::move(quit_closure).Run(); +} + +void CreateAndSwitchToProfile(const std::string& basepath) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + ASSERT_TRUE(profile_manager); + + base::FilePath path = profile_manager->user_data_dir().AppendASCII(basepath); + base::RunLoop run_loop; + profile_manager->CreateProfileAsync( + path, base::BindRepeating(&UnblockOnProfileInitialized, + run_loop.QuitClosure())); + // Run the message loop to allow profile initialization to take place; the + // loop is terminated by UnblockOnProfileInitialized. + run_loop.Run(); + + profiles::SwitchToProfile(path, false, ProfileManager::CreateCallback()); +} + +struct ExistingWinBrowserProfilesSigninUtilTestParams { + ExistingWinBrowserProfilesSigninUtilTestParams( + const std::wstring& email_in_other_profile, + bool cred_provider_used_other_profile, + const std::wstring& current_profile, + const std::wstring& email_in_current_profile, + bool expect_is_started) + : email_in_other_profile(email_in_other_profile), + cred_provider_used_other_profile(cred_provider_used_other_profile), + current_profile(current_profile), + email_in_current_profile(email_in_current_profile), + expect_is_started(expect_is_started) {} + + std::wstring email_in_other_profile; + bool cred_provider_used_other_profile; + std::wstring current_profile; + std::wstring email_in_current_profile; + bool expect_is_started; +}; + +class ExistingWinBrowserProfilesSigninUtilTest + : public BrowserTestHelper, + public InProcessBrowserTest, + public testing::WithParamInterface< + ExistingWinBrowserProfilesSigninUtilTestParams> { + public: + ExistingWinBrowserProfilesSigninUtilTest() + : BrowserTestHelper(L"gaia_id_for_foo_gmail.com", + L"foo@gmail.com", + "lst-123456") {} + + protected: + bool SetUpUserDataDirectory() override { + registry_override_.OverrideRegistry(HKEY_CURRENT_USER); + + signin_util::SetDiceTurnSyncOnHelperDelegateForTesting( + std::unique_ptr<DiceTurnSyncOnHelper::Delegate>( + new TestDiceTurnSyncOnHelperDelegate())); + if (!IsPreTest()) { + SetSigninUtilRegistry(); + } else if (IsPrePreTest() && GetParam().cred_provider_used_other_profile) { + BrowserTestHelper(L"gaia_id_for_bar_gmail.com", L"bar@gmail.com", + "lst-123456") + .SetSigninUtilRegistry(); + } + + return InProcessBrowserTest::SetUpUserDataDirectory(); + } + + private: + base::test::ScopedFeatureList feature_list_; + registry_util::RegistryOverrideManager registry_override_; +}; + +// In PRE_PRE_Run, browser starts for the first time with the initial profile +// dir. If needed by the test, this step can set |email_in_other_profile| as the +// primary account in the profile or it can sign in with credential provider, +// but before this step ends, |current_profile| is created and browser switches +// to that profile just to prepare the browser for the next step. +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, PRE_PRE_Run) { + g_browser_process->local_state()->SetBoolean( + prefs::kBrowserShowProfilePickerOnStartup, false); + + ProfileManager* profile_manager = g_browser_process->profile_manager(); + + Profile* profile = profile_manager->GetLastUsedProfile(); + + ASSERT_EQ(profile_manager->GetInitialProfileDir(), profile->GetBaseName()); + + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + ASSERT_TRUE(identity_manager); + ASSERT_TRUE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync) == + GetParam().cred_provider_used_other_profile); + + if (!GetParam().cred_provider_used_other_profile && + !GetParam().email_in_other_profile.empty()) { + signin::MakePrimaryAccountAvailable( + identity_manager, base::WideToUTF8(GetParam().email_in_other_profile), + signin::ConsentLevel::kSync); + + ASSERT_TRUE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)); + } + + CreateAndSwitchToProfile(base::WideToUTF8(GetParam().current_profile)); +} + +// Browser starts with the |current_profile| profile created in the previous +// step. If needed by the test, this step can set |email_in_current_profile| as +// the primary account in the profile. +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, PRE_Run) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + Profile* profile = profile_manager->GetLastUsedProfile(); + + ASSERT_EQ(GetParam().current_profile, profile->GetBaseName().value()); + + auto* identity_manager = IdentityManagerFactory::GetForProfile(profile); + ASSERT_TRUE(identity_manager); + ASSERT_FALSE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)); + + if (!GetParam().email_in_current_profile.empty()) { + signin::MakePrimaryAccountAvailable( + identity_manager, base::WideToUTF8(GetParam().email_in_current_profile), + signin::ConsentLevel::kSync); + + ASSERT_TRUE( + identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync)); + } +} + +// Before this step runs, refresh token is written into fake registry. Browser +// starts with the |current_profile| profile. Depending on the test case, +// profile may have a primary account. Similarly the other profile(initial +// profile in this case) may have a primary account as well. +IN_PROC_BROWSER_TEST_P(ExistingWinBrowserProfilesSigninUtilTest, Run) { + ProfileManager* profile_manager = g_browser_process->profile_manager(); + Profile* profile = profile_manager->GetLastUsedProfile(); + + ASSERT_EQ(GetParam().current_profile, profile->GetBaseName().value()); + AssertSigninStarted(GetParam().expect_is_started, profile); +} + +INSTANTIATE_TEST_SUITE_P( + AllowCurrentProfile_NoUserSignedIn, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"", + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P( + AllowCurrentProfile_SameUserSignedIn, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"foo@gmail.com", + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P( + DisallowCurrentProfile_DifferentUserSignedIn, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"bar@gmail.com", + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P( + DisallowCurrentProfile_SameUserSignedInDefaultProfile, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"foo@gmail.com", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"", + /*expect_is_started=*/false))); + +INSTANTIATE_TEST_SUITE_P( + AllowCurrentProfile_DifferentUserSignedInDefaultProfile, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"bar@gmail.com", + /*cred_provider_used_other_profile*/ false, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"", + /*expect_is_started=*/true))); + +INSTANTIATE_TEST_SUITE_P( + DisallowCurrentProfile_CredProviderUsedDefaultProfile, + ExistingWinBrowserProfilesSigninUtilTest, + testing::Values(ExistingWinBrowserProfilesSigninUtilTestParams( + /*email_in_other_profile*/ L"", + /*cred_provider_used_other_profile*/ true, + /*current_profile*/ L"profile1", + /*email_in_current_profile=*/L"", + /*expect_is_started=*/false))); diff --git a/chromium/chrome/browser/signin/test_signin_client_builder.cc b/chromium/chrome/browser/signin/test_signin_client_builder.cc new file mode 100644 index 00000000000..9795a5e7e85 --- /dev/null +++ b/chromium/chrome/browser/signin/test_signin_client_builder.cc @@ -0,0 +1,18 @@ +// Copyright 2014 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. + +#include "chrome/browser/signin/test_signin_client_builder.h" + +#include "chrome/browser/profiles/profile.h" +#include "components/signin/public/base/test_signin_client.h" + +namespace signin { + +std::unique_ptr<KeyedService> BuildTestSigninClient( + content::BrowserContext* context) { + return std::make_unique<TestSigninClient>( + static_cast<Profile*>(context)->GetPrefs()); +} + +} // namespace signin diff --git a/chromium/chrome/browser/signin/test_signin_client_builder.h b/chromium/chrome/browser/signin/test_signin_client_builder.h new file mode 100644 index 00000000000..3863c510150 --- /dev/null +++ b/chromium/chrome/browser/signin/test_signin_client_builder.h @@ -0,0 +1,26 @@ +// Copyright 2014 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. + +#ifndef CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_ +#define CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_ + +#include <memory> + +class KeyedService; + +namespace content { +class BrowserContext; +} + +namespace signin { + +// Method to be used by the |ChromeSigninClientFactory| to create a test version +// of the SigninClient +std::unique_ptr<KeyedService> BuildTestSigninClient( + content::BrowserContext* context); + +} // namespace signin + + +#endif // CHROME_BROWSER_SIGNIN_TEST_SIGNIN_CLIENT_BUILDER_H_ diff --git a/chromium/chrome/browser/signin/token_revoker_test_utils.cc b/chromium/chrome/browser/signin/token_revoker_test_utils.cc new file mode 100644 index 00000000000..69b26c881b1 --- /dev/null +++ b/chromium/chrome/browser/signin/token_revoker_test_utils.cc @@ -0,0 +1,36 @@ +// Copyright 2016 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. + +#include "chrome/browser/signin/token_revoker_test_utils.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/net/system_network_context_manager.h" +#include "content/public/test/test_utils.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" + +namespace token_revoker_test_utils { + +RefreshTokenRevoker::RefreshTokenRevoker() + : gaia_fetcher_(this, + gaia::GaiaSource::kChrome, + g_browser_process->system_network_context_manager() + ->GetSharedURLLoaderFactory()) {} + +RefreshTokenRevoker::~RefreshTokenRevoker() { +} + +void RefreshTokenRevoker::Revoke(const std::string& token) { + DVLOG(1) << "Starting RefreshTokenRevoker for token: " << token; + gaia_fetcher_.StartRevokeOAuth2Token(token); + message_loop_runner_ = new content::MessageLoopRunner; + message_loop_runner_->Run(); +} + +void RefreshTokenRevoker::OnOAuth2RevokeTokenCompleted( + GaiaAuthConsumer::TokenRevocationStatus status) { + DVLOG(1) << "TokenRevoker OnOAuth2RevokeTokenCompleted"; + message_loop_runner_->Quit(); +} + +} // namespace token_revoker_test_utils diff --git a/chromium/chrome/browser/signin/token_revoker_test_utils.h b/chromium/chrome/browser/signin/token_revoker_test_utils.h new file mode 100644 index 00000000000..2c1b08a257a --- /dev/null +++ b/chromium/chrome/browser/signin/token_revoker_test_utils.h @@ -0,0 +1,42 @@ +// Copyright 2016 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. + +#ifndef CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_ +#define CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_ + +#include "base/memory/ref_counted.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" + +namespace content { +class MessageLoopRunner; +} + +namespace token_revoker_test_utils { + +// A helper class that takes care of asynchronously revoking a refresh token. +class RefreshTokenRevoker : public GaiaAuthConsumer { + public: + RefreshTokenRevoker(); + + RefreshTokenRevoker(const RefreshTokenRevoker&) = delete; + RefreshTokenRevoker& operator=(const RefreshTokenRevoker&) = delete; + + ~RefreshTokenRevoker() override; + + // Sends a request to Gaia servers to revoke the refresh token. Blocks until + // it is revoked, i.e. until OnOAuth2RevokeTokenCompleted is fired. + void Revoke(const std::string& token); + + // Called when token is revoked. + void OnOAuth2RevokeTokenCompleted( + GaiaAuthConsumer::TokenRevocationStatus status) override; + + private: + GaiaAuthFetcher gaia_fetcher_; + scoped_refptr<content::MessageLoopRunner> message_loop_runner_; +}; + +} // namespace token_revoker_test_utils + +#endif // CHROME_BROWSER_SIGNIN_TOKEN_REVOKER_TEST_UTILS_H_ |