blob: 460037eb7836ba256d04a3cf2d56a6bc3ad15120 [file] [log] [blame]
// 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 "base/strings/strcat.h"
#include "base/test/bind.h"
#include "chrome/browser/federated_learning/floc_id_provider.h"
#include "chrome/browser/federated_learning/floc_id_provider_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/subresource_filter/subresource_filter_browser_test_harness.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/embedder_support/switches.h"
#include "components/federated_learning/features/features.h"
#include "components/history/core/browser/history_service.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
#include "components/subresource_filter/core/common/test_ruleset_utils.h"
#include "content/public/browser/navigation_handle.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/test_frame_navigation_observer.h"
#include "content/public/test/test_host_resolver.h"
#include "content/public/test/test_navigation_observer.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/federated_learning/floc.mojom.h"
namespace {
class FixedFlocIdProvider : public federated_learning::FlocIdProvider {
public:
FixedFlocIdProvider() = default;
~FixedFlocIdProvider() override = default;
blink::mojom::InterestCohortPtr GetInterestCohortForJsApi(
const GURL& url,
const base::Optional<url::Origin>& top_frame_origin) const override {
blink::mojom::InterestCohortPtr cohort =
blink::mojom::InterestCohort::New();
cohort->id = "12345";
cohort->version = "chrome.6.7.8.9";
return cohort;
}
void MaybeRecordFlocToUkm(ukm::SourceId source_id) override {}
};
} // namespace
// Tests behaviors that affect whether the floc API is allowed and/or whether
// the navigation's associated history entry is eligible for floc computation.
class FlocEligibilityBrowserTest
: public subresource_filter::SubresourceFilterBrowserTest {
public:
FlocEligibilityBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(
blink::features::kInterestCohortFeaturePolicy);
}
void SetUpOnMainThread() override {
subresource_filter::SubresourceFilterBrowserTest::SetUpOnMainThread();
https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
https_server_.AddDefaultHandlers(GetChromeTestDataDir());
content::SetupCrossSiteRedirector(&https_server_);
ASSERT_TRUE(https_server_.Start());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
"InterestCohortAPI");
}
// BrowserTestBase::SetUpInProcessBrowserTestFixture
void SetUpInProcessBrowserTestFixture() override {
subscription_ =
BrowserContextDependencyManager::GetInstance()
->RegisterCreateServicesCallbackForTesting(base::BindRepeating(
&FlocEligibilityBrowserTest::OnWillCreateBrowserContextServices,
base::Unretained(this)));
}
std::string InvokeInterestCohortJsApi(
const content::ToRenderFrameHost& adapter) {
return EvalJs(adapter, R"(
if (!(document.interestCohort instanceof Function)) {
'not a function';
} else {
document.interestCohort()
.then(floc => JSON.stringify(floc, Object.keys(floc).sort()))
.catch(error => 'rejected');
}
)")
.ExtractString();
}
// Returns base::nullopt if there's no matching result in the history query.
// Otherwise, the returned base::Optional contains a bit representing whether
// the entry is eligible in floc computation.
base::Optional<bool> QueryFlocEligibleForURL(const GURL& url) {
base::Optional<bool> query_result;
history::QueryOptions options;
options.duplicate_policy = history::QueryOptions::KEEP_ALL_DUPLICATES;
base::RunLoop run_loop;
base::CancelableTaskTracker tracker;
history_service()->QueryHistory(
std::u16string(), options,
base::BindLambdaForTesting([&](history::QueryResults results) {
size_t num_matches = 0;
const size_t* match_index = results.MatchesForURL(url, &num_matches);
if (!num_matches) {
run_loop.Quit();
return;
}
ASSERT_EQ(1u, num_matches);
query_result =
results[*match_index].content_annotations().annotation_flags &
history::VisitContentAnnotationFlag::kFlocEligibleRelaxed;
run_loop.Quit();
}),
&tracker);
run_loop.Run();
return query_result;
}
void DeleteAllHistory() {
base::RunLoop run_loop;
base::CancelableTaskTracker tracker;
HistoryServiceFactory::GetForProfile(browser()->profile(),
ServiceAccessType::EXPLICIT_ACCESS)
->ExpireHistoryBetween(
/*restrict_urls=*/{}, /*begin_time=*/base::Time(),
base::Time::Max(),
/*user_initiated=*/true,
base::BindLambdaForTesting([&]() { run_loop.Quit(); }), &tracker);
run_loop.Run();
}
content::WebContents* web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
history::HistoryService* history_service() {
return HistoryServiceFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS);
}
void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
federated_learning::FlocIdProviderFactory::GetInstance()->SetTestingFactory(
context, base::BindRepeating(
&FlocEligibilityBrowserTest::CreateFixedFlocIdProvider,
base::Unretained(this)));
}
std::unique_ptr<KeyedService> CreateFixedFlocIdProvider(
content::BrowserContext* context) {
return std::make_unique<FixedFlocIdProvider>();
}
void NavigateAndWaitForResourcesCompeletion(const GURL& url,
int expected_complete_resources) {
auto waiter =
std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
web_contents());
ui_test_utils::NavigateToURL(browser(), url);
waiter->AddMinimumCompleteResourcesExpectation(expected_complete_resources);
waiter->Wait();
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
net::EmbeddedTestServer https_server_{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
base::CallbackListSubscription subscription_;
};
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
NotEligibleForHistoryByDefault) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
GURL main_page_url = https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html");
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Expect that the navigation history is not eligible for floc computation.
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_FALSE(query_floc_eligible.value());
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
NotEligibleForHistoryAfterAdResource) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
SetRulesetWithRules(
{subresource_filter::testing::CreateSuffixRule("maybe_ad_script.js")});
GURL main_page_url = https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html");
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Expect that the navigation history is not eligible for floc computation.
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_FALSE(query_floc_eligible.value());
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
EligibleForHistoryAfterApiCall) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
GURL main_page_url = https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html");
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Expect that the navigation history is eligible for floc computation after
// an API call.
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(web_contents()));
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_TRUE(query_floc_eligible.value());
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
NotEligibleForHistoryDueToPermissionsPolicyLegacy) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
GURL main_page_url = https_server_.GetURL(
"a.test",
"/federated_learning/"
"permissions_policy_interest_cohort_none_legacy.html");
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
InvokeInterestCohortJsApi(web_contents());
// Expect that the navigation history is not eligible for floc computation as
// the permissions policy disallows it.
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_FALSE(query_floc_eligible.value());
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
NotEligibleForHistoryDueToPermissionsPolicy) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
GURL main_page_url = https_server_.GetURL(
"a.test",
"/federated_learning/permissions_policy_interest_cohort_none.html");
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
InvokeInterestCohortJsApi(web_contents());
// Expect that the navigation history is not eligible for floc computation as
// the permissions policy disallows it.
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_FALSE(query_floc_eligible.value());
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
NotEligibleForHistoryDueToPrivateIP) {
GURL main_page_url = https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html");
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
InvokeInterestCohortJsApi(web_contents());
// Expect that the navigation history is not eligible for floc computation as
// the IP was not publicly routable.
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_FALSE(query_floc_eligible.value());
}
// The history query result doesn't contain any subframe navigation entries
// (auto & manual).
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
NotEligibleForHistorySubframeCommit) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
GURL main_page_url(https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html"));
GURL auto_subframe_url(https_server_.GetURL("a.test", "/title1.html"));
// Navigate to a page that contains an iframe ("title1.html").
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// The history query result doesn't contain auto subframe navigation entry.
EXPECT_FALSE(QueryFlocEligibleForURL(auto_subframe_url));
// Trigger an user-initiated navigation on the iframe, so that it will show up
// in history.
GURL manual_subframe_url(https_server_.GetURL("a.test", "/title2.html"));
content::NavigateIframeToURL(web_contents(),
/*iframe_id=*/"test", manual_subframe_url);
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(web_contents()));
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(
content::ChildFrameAt(web_contents()->GetMainFrame(), 0)));
// The history query result doesn't contain manual subframe navigation entry
// either.
EXPECT_FALSE(QueryFlocEligibleForURL(manual_subframe_url));
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
SettingFlocAllowedNoopOnDeletedHistory) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
GURL main_page_url(https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html"));
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
EXPECT_TRUE(QueryFlocEligibleForURL(main_page_url));
DeleteAllHistory();
// Expect that attempting to set the "floc allowed" bit will be a no-op if the
// page visit doesn't exist.
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(web_contents()));
EXPECT_FALSE(QueryFlocEligibleForURL(main_page_url));
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest, ApiAllowedByDefault) {
GURL main_page_url(https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html"));
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Navigate the iframe to a cross-origin site.
GURL subframe_url(https_server_.GetURL("b.test", "/title1.html"));
content::NavigateIframeToURL(web_contents(),
/*iframe_id=*/"test", subframe_url);
content::RenderFrameHost* child =
content::ChildFrameAt(web_contents()->GetMainFrame(), 0);
// Expect that both main frame and subframe are allowed to access floc.
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(web_contents()));
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(child));
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
ApiNotAllowedDueToInsecureContext) {
GURL main_page_url(embedded_test_server()->GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html"));
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Navigate the iframe to a https site.
GURL subframe_url(https_server_.GetURL("b.test", "/title1.html"));
content::NavigateIframeToURL(web_contents(),
/*iframe_id=*/"test", subframe_url);
content::RenderFrameHost* child =
content::ChildFrameAt(web_contents()->GetMainFrame(), 0);
// Expect that both main frame and subframe are not allowed to access floc.
EXPECT_EQ("not a function", InvokeInterestCohortJsApi(web_contents()));
EXPECT_EQ("not a function", InvokeInterestCohortJsApi(child));
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
ApiNotAllowedDueToPermissionsPolicy) {
GURL main_page_url(https_server_.GetURL(
"a.test",
"/federated_learning/"
"permissions_policy_interest_cohort_none_legacy.html"));
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Navigate the iframe to a cross-origin site.
GURL subframe_url(https_server_.GetURL("b.test", "/title1.html"));
content::NavigateIframeToURL(web_contents(),
/*iframe_id=*/"test", subframe_url);
content::RenderFrameHost* child =
content::ChildFrameAt(web_contents()->GetMainFrame(), 0);
// Expect that both main frame and subframe are not allowed to access floc.
EXPECT_EQ("rejected", InvokeInterestCohortJsApi(web_contents()));
EXPECT_EQ("rejected", InvokeInterestCohortJsApi(child));
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
ApiNotAllowedInSubframeDueToPermissionsPolicySelf) {
GURL main_page_url(https_server_.GetURL(
"a.test",
"/federated_learning/"
"permissions_policy_interest_cohort_self_legacy.html"));
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Navigate the iframe to a cross-origin site.
GURL subframe_url(https_server_.GetURL("b.test", "/title1.html"));
content::NavigateIframeToURL(web_contents(),
/*iframe_id=*/"test", subframe_url);
content::RenderFrameHost* child =
content::ChildFrameAt(web_contents()->GetMainFrame(), 0);
// Expect that only the main frame can access floc.
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(web_contents()));
EXPECT_EQ("rejected", InvokeInterestCohortJsApi(child));
}
IN_PROC_BROWSER_TEST_F(FlocEligibilityBrowserTest,
ApiNotAllowedInDetachedDocument) {
ui_test_utils::NavigateToURL(
browser(),
https_server_.GetURL(
"a.test",
"/federated_learning/interest_cohort_api_in_detached_document.html"));
EXPECT_EQ(
"[error from subframe document] InvalidAccessError: Failed to execute "
"'interestCohort' on 'Document': A browsing context is required when "
"calling document.interestCohort.",
EvalJs(web_contents()->GetMainFrame(), R"(
document.body.textContent
)")
.ExtractString());
}
class FlocEligibilityBrowserTestPagesWithAdResourcesDefaultIncluded
: public FlocEligibilityBrowserTest {
public:
FlocEligibilityBrowserTestPagesWithAdResourcesDefaultIncluded() {
scoped_feature_list_.Reset();
scoped_feature_list_.InitWithFeatures(
{blink::features::kInterestCohortFeaturePolicy,
federated_learning::
kFlocPagesWithAdResourcesDefaultIncludedInFlocComputation},
{});
}
};
IN_PROC_BROWSER_TEST_F(
FlocEligibilityBrowserTestPagesWithAdResourcesDefaultIncluded,
EligibleForHistoryAfterAdResource) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
SetRulesetWithRules(
{subresource_filter::testing::CreateSuffixRule("maybe_ad_script.js")});
GURL main_page_url = https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html");
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Expect that the navigation history is eligible for floc computation as the
// page contains an ad resource.
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_TRUE(query_floc_eligible.value());
}
IN_PROC_BROWSER_TEST_F(
FlocEligibilityBrowserTestPagesWithAdResourcesDefaultIncluded,
NotEligibleForHistoryAfterNonAdResource) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
GURL main_page_url = https_server_.GetURL(
"a.test", "/federated_learning/page_with_script_and_iframe.html");
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Expect that the navigation history is not eligible for floc computation.
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_FALSE(query_floc_eligible.value());
}
class FlocEligibilityBrowserTestChromePermissionsPolicyDisabled
: public FlocEligibilityBrowserTest {
public:
FlocEligibilityBrowserTestChromePermissionsPolicyDisabled() {
scoped_feature_list_.Reset();
scoped_feature_list_.InitAndDisableFeature(
blink::features::kInterestCohortFeaturePolicy);
}
};
IN_PROC_BROWSER_TEST_F(
FlocEligibilityBrowserTestChromePermissionsPolicyDisabled,
PermissionsPolicyFeatureNotAvailable) {
GURL main_page_url(https_server_.GetURL("a.test", "/title1.html"));
ui_test_utils::NavigateToURL(browser(), main_page_url);
EXPECT_FALSE(EvalJs(web_contents(), R"(
document.featurePolicy.features().includes("interest-cohort")
)")
.ExtractBool());
}
// Try configuring the permissions policy anyway. Check that the API succeeds
// and the history is eligible for floc computation.
IN_PROC_BROWSER_TEST_F(
FlocEligibilityBrowserTestChromePermissionsPolicyDisabled,
PermissionsPolicyFeatureNotEffective) {
net::IPAddress::ConsiderLoopbackIPToBePubliclyRoutableForTesting();
GURL main_page_url(https_server_.GetURL(
"a.test",
"/federated_learning/"
"permissions_policy_interest_cohort_none_legacy.html"));
// Three resources in the main frame and one favicon.
NavigateAndWaitForResourcesCompeletion(main_page_url, 4);
// Navigate the iframe to a cross-origin site.
GURL subframe_url(https_server_.GetURL("b.test", "/title1.html"));
content::NavigateIframeToURL(web_contents(),
/*iframe_id=*/"test", subframe_url);
content::RenderFrameHost* child =
content::ChildFrameAt(web_contents()->GetMainFrame(), 0);
// Expect that both main frame and subframe are allowed to access floc.
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(web_contents()));
EXPECT_EQ("{\"id\":\"12345\",\"version\":\"chrome.6.7.8.9\"}",
InvokeInterestCohortJsApi(child));
// Expect that the navigation history is eligible for floc computation.
base::Optional<bool> query_floc_eligible =
QueryFlocEligibleForURL(main_page_url);
EXPECT_TRUE(query_floc_eligible);
EXPECT_TRUE(query_floc_eligible.value());
}