blob: 4dc2a70d6bb52aa7e1c2d0123ce4597419958c58 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include "base/functional/callback.h"
#include "base/memory/ref_counted.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/browsing_data/browsing_data_api.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/account_reconcilor_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/sync/sync_ui_util.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/platform_browser_test.h"
#include "components/services/storage/public/mojom/local_storage_control.mojom.h"
#include "components/services/storage/public/mojom/storage_usage_info.mojom.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_test_utils.h"
#include "components/sync/service/sync_service.h"
#include "components/sync/service/sync_user_settings.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/test/test_extension_dir.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "net/base/features.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/dom_storage/storage_area.mojom.h"
#include "url/gurl.h"
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "chrome/browser/ui/browser.h"
#endif
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
using extensions::api_test_utils::RunFunctionAndReturnSingleResult;
namespace {
class ExtensionBrowsingDataTest : public PlatformBrowserTest {
public:
ExtensionBrowsingDataTest() = default;
ExtensionBrowsingDataTest(const ExtensionBrowsingDataTest&) = delete;
ExtensionBrowsingDataTest& operator=(const ExtensionBrowsingDataTest&) =
delete;
~ExtensionBrowsingDataTest() override = default;
Profile* GetProfile() { return chrome_test_utils::GetProfile(this); }
};
class ExtensionBrowsingDataTestWithStoragePartitioning
: public ExtensionBrowsingDataTest {
public:
ExtensionBrowsingDataTestWithStoragePartitioning() {
scoped_feature_list_.InitAndEnableFeature(
net::features::kThirdPartyStoragePartitioning);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// TODO(http://crbug.com/1266606): appcache is a noop and should be removed.
const char kRemoveEverythingArguments[] =
R"([{"since": 1000}, {
"appcache": true, "cache": true, "cookies": true,
"downloads": true, "fileSystems": true, "formData": true,
"history": true, "indexedDB": true, "localStorage": true,
"serverBoundCertificates": true, "passwords": true,
"pluginData": true, "serviceWorkers": true, "cacheStorage": true,
"webSQL": true
}])";
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
// Sets the SAPISID Gaia cookie, which is monitored by the AccountReconcilor.
bool SetGaiaCookieForProfile(Profile* profile) {
GURL google_url = GaiaUrls::GetInstance()->secure_google_url();
auto cookie = net::CanonicalCookie::CreateUnsafeCookieForTesting(
"SAPISID", std::string(), "." + google_url.GetHost(), "/", base::Time(),
base::Time(), base::Time(), base::Time(),
/*secure=*/true, false, net::CookieSameSite::NO_RESTRICTION,
net::COOKIE_PRIORITY_DEFAULT);
base::test::TestFuture<net::CookieAccessResult> set_cookie_future;
network::mojom::CookieManager* cookie_manager =
profile->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess();
cookie_manager->SetCanonicalCookie(
*cookie, google_url, net::CookieOptions::MakeAllInclusive(),
mojo::WrapCallbackWithDefaultInvokeIfNotRun(
set_cookie_future.GetCallback(),
net::CookieAccessResult(
net::CookieInclusionStatus::MakeFromReasonsForTesting(
/*exclusions=*/{net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_UNKNOWN_ERROR}))));
return set_cookie_future.Get().status.IsInclude();
}
#endif
} // namespace
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
// Test that Sync is not paused when browsing data is cleared.
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, Syncing) {
Profile* profile = GetProfile();
// Set a Gaia cookie.
ASSERT_TRUE(SetGaiaCookieForProfile(profile));
// Set a Sync account and a secondary account.
const char kPrimaryAccountEmail[] = "primary@email.com";
const char kSecondaryAccountEmail[] = "secondary@email.com";
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
AccountInfo primary_account_info = signin::MakePrimaryAccountAvailable(
identity_manager, kPrimaryAccountEmail, signin::ConsentLevel::kSync);
AccountInfo secondary_account_info =
signin::MakeAccountAvailable(identity_manager, kSecondaryAccountEmail);
// Sync is running.
syncer::SyncService* sync_service =
SyncServiceFactory::GetForProfile(profile);
sync_service->GetUserSettings()->SetInitialSyncFeatureSetupComplete(
syncer::SyncFirstSetupCompleteSource::BASIC_FLOW);
ASSERT_EQ(SyncStatusMessageType::kSynced, GetSyncStatusMessageType(profile));
// Clear browsing data.
auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
EXPECT_FALSE(RunFunctionAndReturnSingleResult(
function.get(), kRemoveEverythingArguments, GetProfile()));
// Check that the Sync token was not revoked.
EXPECT_TRUE(identity_manager->HasAccountWithRefreshToken(
primary_account_info.account_id));
EXPECT_FALSE(
identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
primary_account_info.account_id));
// Check that the secondary token was revoked.
EXPECT_FALSE(identity_manager->HasAccountWithRefreshToken(
secondary_account_info.account_id));
}
// Test that Sync remained in error when browsing data is cleared if Sync was in
// authentication error.
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, SyncError) {
Profile* profile = GetProfile();
// Set a Gaia cookie.
ASSERT_TRUE(SetGaiaCookieForProfile(profile));
// Set a Sync account with authentication error.
const char kAccountEmail[] = "account@email.com";
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
AccountInfo account_info = signin::MakePrimaryAccountAvailable(
identity_manager, kAccountEmail, signin::ConsentLevel::kSync);
signin::UpdatePersistentErrorOfRefreshTokenForAccount(
identity_manager, account_info.account_id,
GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
GoogleServiceAuthError::InvalidGaiaCredentialsReason::
CREDENTIALS_REJECTED_BY_SERVER));
// Sync is not running.
ASSERT_NE(SyncStatusMessageType::kSynced, GetSyncStatusMessageType(profile));
// Clear browsing data.
auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
EXPECT_FALSE(RunFunctionAndReturnSingleResult(
function.get(), kRemoveEverythingArguments, GetProfile()));
// Check that the account was not removed and Sync remains in auth error.
EXPECT_TRUE(
identity_manager->HasAccountWithRefreshToken(account_info.account_id));
EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason::
CREDENTIALS_REJECTED_BY_SERVER,
identity_manager
->GetErrorStateOfRefreshTokenForAccount(account_info.account_id)
.GetInvalidGaiaCredentialsReason());
}
// Test that the tokens are revoked when browsing data is cleared when there is
// no primary account.
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, NotSyncing) {
Profile* profile = GetProfile();
// Set a Gaia cookie.
ASSERT_TRUE(SetGaiaCookieForProfile(profile));
// Set a non-Sync account.
const char kAccountEmail[] = "account@email.com";
auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
AccountInfo account_info =
signin::MakeAccountAvailable(identity_manager, kAccountEmail);
// Clear browsing data.
auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
EXPECT_FALSE(RunFunctionAndReturnSingleResult(
function.get(), kRemoveEverythingArguments, GetProfile()));
// Check that the account was removed.
EXPECT_FALSE(
identity_manager->HasAccountWithRefreshToken(account_info.account_id));
}
#endif
void CreateLocalStorageForKey(Profile* profile, const blink::StorageKey& key) {
auto* local_storage_control =
profile->GetDefaultStoragePartition()->GetLocalStorageControl();
mojo::Remote<blink::mojom::StorageArea> area;
local_storage_control->BindStorageArea(key,
area.BindNewPipeAndPassReceiver());
{
base::test::TestFuture<bool> put_future;
area->Put({'k', 'e', 'y'}, {'v', 'a', 'l', 'u', 'e'}, std::nullopt,
"source", put_future.GetCallback());
ASSERT_TRUE(put_future.Get());
}
}
std::vector<storage::mojom::StorageUsageInfoPtr> GetLocalStorage(
Profile* profile) {
auto* local_storage_control =
profile->GetDefaultStoragePartition()->GetLocalStorageControl();
base::test::TestFuture<std::vector<storage::mojom::StorageUsageInfoPtr>>
get_usage_future;
local_storage_control->GetUsage(get_usage_future.GetCallback());
return get_usage_future.Take();
}
bool UsageInfosHasStorageKey(
const std::vector<storage::mojom::StorageUsageInfoPtr>& usage_infos,
const blink::StorageKey& key) {
auto it = std::ranges::find_if(
usage_infos, [&key](const storage::mojom::StorageUsageInfoPtr& info) {
return info->storage_key == key;
});
return it != usage_infos.end();
}
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, DeleteLocalStorageAll) {
const blink::StorageKey key1 =
blink::StorageKey::CreateFromStringForTesting("https://example.com");
const blink::StorageKey key2 =
blink::StorageKey::CreateFromStringForTesting("https://other.com");
// Create some local storage for each of the origins.
CreateLocalStorageForKey(GetProfile(), key1);
CreateLocalStorageForKey(GetProfile(), key2);
// Verify that the data is actually stored.
auto usage_infos = GetLocalStorage(GetProfile());
EXPECT_EQ(2U, usage_infos.size());
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key1));
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key2));
// Clear the data for everything.
auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
EXPECT_FALSE(RunFunctionAndReturnSingleResult(
function.get(), kRemoveEverythingArguments, GetProfile()));
usage_infos = GetLocalStorage(GetProfile());
EXPECT_EQ(0U, usage_infos.size());
}
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, DeleteLocalStorageIncognito) {
const blink::StorageKey key1 =
blink::StorageKey::CreateFromStringForTesting("https://example.com");
const blink::StorageKey key2 =
blink::StorageKey::CreateFromStringForTesting("https://other.com");
// Create some local storage for each of the origins.
auto* incognito_profile = GetProfile()->GetPrimaryOTRProfile(true);
CreateLocalStorageForKey(incognito_profile, key1);
CreateLocalStorageForKey(incognito_profile, key2);
// Verify that the data is actually stored.
auto usage_infos = GetLocalStorage(incognito_profile);
EXPECT_EQ(2U, usage_infos.size());
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key1));
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key2));
// Clear the data for everything.
auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
EXPECT_FALSE(RunFunctionAndReturnSingleResult(
function.get(), kRemoveEverythingArguments, incognito_profile));
usage_infos = GetLocalStorage(incognito_profile);
EXPECT_EQ(0U, usage_infos.size());
}
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, DeleteLocalStorageOrigin) {
const blink::StorageKey key1 =
blink::StorageKey::CreateFromStringForTesting("https://example.com");
const blink::StorageKey key2 =
blink::StorageKey::CreateFromStringForTesting("https://other.com");
// Create some local storage for each of the origins.
CreateLocalStorageForKey(GetProfile(), key1);
CreateLocalStorageForKey(GetProfile(), key2);
// Verify that the data is actually stored.
auto usage_infos = GetLocalStorage(GetProfile());
EXPECT_EQ(2U, usage_infos.size());
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key1));
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key2));
// Clear the data only for example.com.
auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
const char removeArgs[] =
R"([{
"origins": ["https://example.com"]
}, {
"localStorage": true
}])";
EXPECT_FALSE(RunFunctionAndReturnSingleResult(function.get(), removeArgs,
GetProfile()));
usage_infos = GetLocalStorage(GetProfile());
EXPECT_EQ(1U, usage_infos.size());
EXPECT_FALSE(UsageInfosHasStorageKey(usage_infos, key1));
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key2));
}
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTestWithStoragePartitioning,
DeleteLocalStoragePartitioned) {
ASSERT_TRUE(blink::StorageKey::IsThirdPartyStoragePartitioningEnabled());
const auto kOrigin = url::Origin::Create(GURL("https://example.com"));
const auto kDifferentOrigin = url::Origin::Create(GURL("https://other.com"));
const auto kDifferentSubdomain =
url::Origin::Create(GURL("https://maps.example.com"));
const auto kAnotherOrigin =
url::Origin::Create(GURL("https://something.com"));
// First-party key for the origin being deleted.
auto key1 =
blink::StorageKey::Create(kOrigin, net::SchemefulSite(kOrigin),
blink::mojom::AncestorChainBit::kSameSite);
// Third-party embedded on the origin being deleted.
auto key2 =
blink::StorageKey::Create(kDifferentOrigin, net::SchemefulSite(kOrigin),
blink::mojom::AncestorChainBit::kCrossSite);
// Cross-site same origin embedded on the origin being deleted.
auto key3 =
blink::StorageKey::Create(kOrigin, net::SchemefulSite(kOrigin),
blink::mojom::AncestorChainBit::kCrossSite);
// Third-party same origin embedded on a different site.
auto key4 =
blink::StorageKey::Create(kOrigin, net::SchemefulSite(kDifferentOrigin),
blink::mojom::AncestorChainBit::kCrossSite);
// First-party key for an origin not being deleted.
auto key5 = blink::StorageKey::Create(
kDifferentOrigin, net::SchemefulSite(kDifferentOrigin),
blink::mojom::AncestorChainBit::kSameSite);
// First-party key for a different subdomain for the origin being deleted.
auto key6 = blink::StorageKey::Create(
kDifferentSubdomain, net::SchemefulSite(kDifferentSubdomain),
blink::mojom::AncestorChainBit::kSameSite);
// Third-party key with a top-level-site equal to a different subdomain for
// the origin being deleted.
auto key7 = blink::StorageKey::Create(
kAnotherOrigin, net::SchemefulSite(kDifferentSubdomain),
blink::mojom::AncestorChainBit::kCrossSite);
// Cross-site different subdomain origin embedded with itself as the top-level
// site.
auto key8 = blink::StorageKey::Create(
kDifferentSubdomain, net::SchemefulSite(kDifferentSubdomain),
blink::mojom::AncestorChainBit::kCrossSite);
std::vector<blink::StorageKey> keys = {key1, key2, key3, key4,
key5, key6, key7, key8};
// Create some local storage for each of the keys.
for (const auto& key : keys) {
CreateLocalStorageForKey(GetProfile(), key);
}
// Verify that the data is actually stored.
auto usage_infos = GetLocalStorage(GetProfile());
EXPECT_EQ(keys.size(), usage_infos.size());
for (const auto& key : keys) {
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key));
}
// Clear the data for example.com.
auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
const char removeArgs[] =
R"([{
"origins": ["https://example.com"]
}, {
"localStorage": true
}])";
EXPECT_FALSE(RunFunctionAndReturnSingleResult(function.get(), removeArgs,
GetProfile()));
usage_infos = GetLocalStorage(GetProfile());
EXPECT_EQ(3U, usage_infos.size());
EXPECT_FALSE(UsageInfosHasStorageKey(usage_infos, key1));
EXPECT_FALSE(UsageInfosHasStorageKey(usage_infos, key2));
EXPECT_FALSE(UsageInfosHasStorageKey(usage_infos, key3));
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key4));
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key5));
EXPECT_TRUE(UsageInfosHasStorageKey(usage_infos, key6));
EXPECT_FALSE(UsageInfosHasStorageKey(usage_infos, key7));
EXPECT_FALSE(UsageInfosHasStorageKey(usage_infos, key8));
}
using BrowsingDataApiTest = extensions::ExtensionApiTest;
IN_PROC_BROWSER_TEST_F(BrowsingDataApiTest, ValidateFilters) {
static constexpr char kManifest[] =
R"({
"name": "Test",
"manifest_version": 3,
"version": "0.1",
"background": {"service_worker": "background.js"},
"permissions": ["browsingData"]
})";
static constexpr char kBackgroundJs[] = R"(chrome.test.runTests([
async function originFilter() {
await chrome.browsingData.remove(
{'origins': ['https://example.com']},
{'cookies': true});
chrome.test.succeed();
},
async function emptyOriginsFilter() {
const expectedError = new RegExp(
'.* Array must have at least 1 items; found 0.');
chrome.test.assertThrows(
chrome.browsingData.remove,
chrome.browsingData,
[{'origins': []}, {'cookies': true}],
expectedError);
chrome.test.succeed();
},
]);)";
extensions::TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
}