| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/signin/dice_signed_in_profile_creator.h" |
| |
| #include "base/barrier_closure.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.h" |
| #include "base/test/test_file_util.h" |
| #include "chrome/browser/enterprise/profile_management/profile_management_features.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/base/signin_pref_names.h" |
| #include "components/signin/public/identity_manager/identity_test_environment.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "services/network/public/mojom/cookie_manager.mojom.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace { |
| |
| const char16_t kProfileTestName[] = u"profile_test_name"; |
| |
| struct CookieTestParam { |
| bool enable_third_party_management_feature; |
| bool setup_cookies_to_move; |
| }; |
| |
| const CookieTestParam kTestCases[] = {{true, true}, |
| {true, false}, |
| {false, false}, |
| {false, true}}; |
| |
| void CreateCookies( |
| Profile* profile, |
| const std::map<std::string, std::string> cookie_url_and_name) { |
| network::mojom::CookieManager* cookie_manager = |
| profile->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess(); |
| |
| base::RunLoop run_loop; |
| base::RepeatingClosure barrier = base::BarrierClosure( |
| cookie_url_and_name.size(), |
| base::BindLambdaForTesting([&]() { run_loop.Quit(); })); |
| |
| for (const auto& url_name : cookie_url_and_name) { |
| GURL url(url_name.first); |
| std::unique_ptr<net::CanonicalCookie> cookie = |
| net::CanonicalCookie::CreateSanitizedCookie( |
| url, url_name.second, "A=" + url_name.second, url.host(), |
| url.path(), base::Time::Now(), base::Time::Max(), base::Time::Now(), |
| url.SchemeIsCryptographic(), false, |
| net::CookieSameSite::NO_RESTRICTION, net::COOKIE_PRIORITY_DEFAULT, |
| std::nullopt); |
| cookie_manager->SetCanonicalCookie( |
| *cookie, url, net::CookieOptions::MakeAllInclusive(), |
| base::BindLambdaForTesting( |
| [&](net::CookieAccessResult access_result) { barrier.Run(); })); |
| } |
| |
| run_loop.Run(); |
| } |
| |
| 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(); |
| } |
| |
| 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 testing::WithParamInterface<CookieTestParam>, |
| public ProfileManagerObserver { |
| public: |
| DiceSignedInProfileCreatorTest() |
| : local_state_(TestingBrowserProcess::GetGlobal()) { |
| #if !BUILDFLAG(IS_FUCHSIA) |
| scoped_feature_list_.InitWithFeatureState( |
| profile_management::features::kThirdPartyProfileManagement, |
| GetParam().enable_third_party_management_feature); |
| #endif |
| auto profile_manager_unique = std::make_unique<UnittestProfileManager>( |
| base::CreateUniqueTempDirectoryScopedToTest()); |
| 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); |
| } |
| |
| void DeleteProfiles() { |
| identity_test_env_profile_adaptor_.reset(); |
| |
| // Delete the profile first to make sure all observers to the profile |
| // manager are cleared to avoid heap-use-after-free when the observer try to |
| // stop observing the manager. |
| profile_.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(); |
| } |
| |
| void SetupCookiesToMove() { |
| if (!GetParam().setup_cookies_to_move) { |
| return; |
| } |
| // Add some cookies |
| CreateCookies(profile_.get(), {{"https://google.com", "oldgoogle0"}, |
| {"https://example.com", "oldexample0"}}); |
| CreateCookies(profile_.get(), {{"https://google.com", "validgoogle1"}, |
| {"https://example.com", "validexample1"}}); |
| CreateCookies(profile_.get(), {{"https://google.com", "newgoogle2"}, |
| {"https://example.com", "newexample2"}}); |
| |
| profile_->GetPrefs()->SetString(prefs::kSigninInterceptionIDPCookiesUrl, |
| "https://www.google.com/"); |
| } |
| |
| void VerifyCookiesMoved() { |
| if (!GetParam().setup_cookies_to_move) { |
| return; |
| } |
| GURL url("https://www.google.com/"); |
| net::CookieList cookies_source_profile; |
| net::CookieList cookies_new_profile; |
| { |
| network::mojom::CookieManager* cookie_manager = |
| profile_->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess(); |
| base::RunLoop loop; |
| cookie_manager->GetAllCookies( |
| base::BindLambdaForTesting([&](const net::CookieList& cookies) { |
| cookies_source_profile = cookies; |
| loop.Quit(); |
| })); |
| loop.Run(); |
| } |
| { |
| network::mojom::CookieManager* cookie_manager = |
| added_profile_->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess(); |
| base::RunLoop loop; |
| cookie_manager->GetAllCookies( |
| base::BindLambdaForTesting([&](const net::CookieList& cookies) { |
| cookies_new_profile = cookies; |
| loop.Quit(); |
| })); |
| loop.Run(); |
| } |
| |
| if (!GetParam().enable_third_party_management_feature) { |
| EXPECT_EQ(6u, cookies_source_profile.size()); |
| EXPECT_TRUE(cookies_new_profile.empty()); |
| return; |
| } |
| // Third party management feature not enabled on fuchsia. |
| #if !BUILDFLAG(IS_FUCHSIA) |
| return; |
| #endif |
| |
| EXPECT_EQ(3u, cookies_source_profile.size()); |
| EXPECT_EQ(3u, cookies_new_profile.size()); |
| |
| for (const auto& cookie : cookies_new_profile) { |
| EXPECT_TRUE(cookie.IsDomainMatch(url.host())); |
| EXPECT_TRUE(cookie.Name() == "oldgoogle0" || |
| cookie.Name() == "validgoogle1" || |
| cookie.Name() == "newgoogle1"); |
| } |
| } |
| |
| private: |
| content::BrowserTaskEnvironment task_environment_; |
| ScopedTestingLocalState local_state_; |
| raw_ptr<UnittestProfileManager, DanglingUntriaged> profile_manager_ = nullptr; |
| std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> |
| identity_test_env_profile_adaptor_; |
| std::unique_ptr<TestingProfile> profile_; |
| raw_ptr<Profile, DanglingUntriaged> signed_in_profile_ = nullptr; |
| raw_ptr<Profile, DanglingUntriaged> added_profile_ = nullptr; |
| base::OnceClosure profile_added_closure_; |
| bool creator_callback_called_ = false; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_P(DiceSignedInProfileCreatorTest, CreateWithTokensLoaded) { |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("bob@example.com"); |
| size_t kTestIcon = profiles::GetModernAvatarIconStartIndex(); |
| |
| SetupCookiesToMove(); |
| base::RunLoop loop; |
| std::unique_ptr<DiceSignedInProfileCreator> creator = |
| std::make_unique<DiceSignedInProfileCreator>( |
| profile(), account_info.account_id, kProfileTestName, kTestIcon, |
| 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_FALSE(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); |
| EXPECT_EQ(kProfileTestName, entry->GetLocalProfileName()); |
| EXPECT_EQ(kTestIcon, entry->GetAvatarIconIndex()); |
| VerifyCookiesMoved(); |
| } |
| |
| TEST_P(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()); |
| SetupCookiesToMove(); |
| std::unique_ptr<DiceSignedInProfileCreator> creator = |
| std::make_unique<DiceSignedInProfileCreator>( |
| profile(), account_info.account_id, std::u16string(), std::nullopt, |
| 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)); |
| VerifyCookiesMoved(); |
| } |
| |
| // Deleting the creator while it is running does not crash. |
| TEST_P(DiceSignedInProfileCreatorTest, DeleteWhileCreating) { |
| AccountInfo account_info = |
| identity_test_env()->MakeAccountAvailable("bob@example.com"); |
| SetupCookiesToMove(); |
| std::unique_ptr<DiceSignedInProfileCreator> creator = |
| std::make_unique<DiceSignedInProfileCreator>( |
| profile(), account_info.account_id, std::u16string(), std::nullopt, |
| 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_P(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()); |
| SetupCookiesToMove(); |
| std::unique_ptr<DiceSignedInProfileCreator> creator = |
| std::make_unique<DiceSignedInProfileCreator>( |
| profile(), account_info.account_id, std::u16string(), std::nullopt, |
| 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()); |
| VerifyCookiesMoved(); |
| |
| DeleteProfiles(); |
| creator_loop.Run(); |
| |
| // The callback is called with nullptr profile. |
| EXPECT_TRUE(creator_callback_called()); |
| EXPECT_FALSE(signed_in_profile()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| DiceSignedInProfileCreatorTest, |
| ::testing::ValuesIn(kTestCases)); |