blob: 200aac52b44699c19df3ce67385fae609b57fb0e [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/interest_group/interest_group_manager_impl.h"
#include <memory>
#include <string>
#include <vector>
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/json/json_string_value_serializer.h"
#include "base/json/values_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/observer_list.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/sequence_bound.h"
#include "base/time/time.h"
#include "components/cbor/diagnostic_writer.h"
#include "components/cbor/writer.h"
#include "content/browser/interest_group/interest_group_storage.h"
#include "content/browser/interest_group/interest_group_update.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/mojom/client_security_state.mojom.h"
#include "third_party/blink/public/common/interest_group/interest_group.h"
#include "url/gurl.h"
namespace content {
namespace {
// The maximum number of active report requests at a time.
constexpr int kMaxActiveReportRequests = 5;
// The maximum number of report URLs that can be stored in `report_requests_`
// queue.
constexpr int kMaxReportQueueLength = 1000;
// The maximum amount of time allowed to fetch report requests in the queue.
constexpr base::TimeDelta kMaxReportingRoundDuration = base::Minutes(10);
// The time interval to wait before sending the next report after sending one.
constexpr base::TimeDelta kReportingInterval = base::Milliseconds(50);
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("auction_report_sender", R"(
semantics {
sender: "Interest group based Ad Auction report"
description:
"Facilitates reporting the result of an in-browser interest group "
"based ad auction to an auction participant. "
"See https://github.com/WICG/turtledove/blob/main/FLEDGE.md"
trigger:
"Requested after running a in-browser interest group based ad "
"auction to report the auction result back to auction participants."
data: "URL associated with an interest group or seller."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting:
"These requests are controlled by a feature flag that is off by "
"default now. When enabled, they can be disabled by the Privacy"
" Sandbox setting."
policy_exception_justification:
"These requests are triggered by a website."
})");
// Makes an uncredentialed request and creates a SimpleURLLoader for it. Returns
// the SimpleURLLoader which will be used to report the result of an in-browser
// interest group based ad auction to an auction participant.
std::unique_ptr<network::SimpleURLLoader> BuildSimpleUrlLoader(
GURL url,
const url::Origin& frame_origin,
const network::mojom::ClientSecurityState& client_security_state) {
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = std::move(url);
resource_request->redirect_mode = network::mojom::RedirectMode::kError;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->request_initiator = frame_origin;
resource_request->trusted_params = network::ResourceRequest::TrustedParams();
resource_request->trusted_params->isolation_info =
net::IsolationInfo::CreateTransient();
resource_request->trusted_params->client_security_state =
client_security_state.Clone();
auto simple_url_loader = network::SimpleURLLoader::Create(
std::move(resource_request), kTrafficAnnotation);
simple_url_loader->SetTimeoutDuration(base::Seconds(30));
return simple_url_loader;
}
std::vector<InterestGroupManager::InterestGroupDataKey>
ConvertOwnerJoinerPairsToDataKeys(
std::vector<std::pair<url::Origin, url::Origin>> owner_joiner_pairs) {
std::vector<InterestGroupManager::InterestGroupDataKey> data_keys;
for (auto& key : owner_joiner_pairs) {
data_keys.emplace_back(
InterestGroupManager::InterestGroupDataKey{key.first, key.second});
}
return data_keys;
}
} // namespace
InterestGroupManagerImpl::ReportRequest::ReportRequest() = default;
InterestGroupManagerImpl::ReportRequest::~ReportRequest() = default;
InterestGroupManagerImpl::AdAuctionDataLoaderState::AdAuctionDataLoaderState() =
default;
InterestGroupManagerImpl::AdAuctionDataLoaderState::AdAuctionDataLoaderState(
AdAuctionDataLoaderState&& state) = default;
InterestGroupManagerImpl::AdAuctionDataLoaderState::
~AdAuctionDataLoaderState() = default;
InterestGroupManagerImpl::InterestGroupManagerImpl(
const base::FilePath& path,
bool in_memory,
ProcessMode process_mode,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
KAnonymityServiceDelegate* k_anonymity_service)
: impl_(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN}),
in_memory ? base::FilePath() : path),
auction_process_manager_(
base::WrapUnique(process_mode == ProcessMode::kDedicated
? static_cast<AuctionProcessManager*>(
new DedicatedAuctionProcessManager())
: new InRendererAuctionProcessManager())),
update_manager_(this, std::move(url_loader_factory)),
k_anonymity_manager_(std::make_unique<InterestGroupKAnonymityManager>(
this,
k_anonymity_service)),
max_active_report_requests_(kMaxActiveReportRequests),
max_report_queue_length_(kMaxReportQueueLength),
reporting_interval_(kReportingInterval),
max_reporting_round_duration_(kMaxReportingRoundDuration) {}
InterestGroupManagerImpl::~InterestGroupManagerImpl() = default;
void InterestGroupManagerImpl::GetAllInterestGroupJoiningOrigins(
base::OnceCallback<void(std::vector<url::Origin>)> callback) {
impl_.AsyncCall(&InterestGroupStorage::GetAllInterestGroupJoiningOrigins)
.Then(std::move(callback));
}
void InterestGroupManagerImpl::GetAllInterestGroupDataKeys(
base::OnceCallback<void(std::vector<InterestGroupDataKey>)> callback) {
impl_.AsyncCall(&InterestGroupStorage::GetAllInterestGroupOwnerJoinerPairs)
.Then(base::BindOnce(&ConvertOwnerJoinerPairsToDataKeys)
.Then(std::move(callback)));
}
void InterestGroupManagerImpl::RemoveInterestGroupsByDataKey(
InterestGroupDataKey data_key,
base::OnceClosure callback) {
impl_
.AsyncCall(
&InterestGroupStorage::RemoveInterestGroupsMatchingOwnerAndJoiner)
.WithArgs(data_key.owner, data_key.joining_origin)
.Then(std::move(callback));
}
void InterestGroupManagerImpl::CheckPermissionsAndJoinInterestGroup(
blink::InterestGroup group,
const GURL& joining_url,
const url::Origin& frame_origin,
const net::NetworkIsolationKey& network_isolation_key,
bool report_result_only,
network::mojom::URLLoaderFactory& url_loader_factory,
blink::mojom::AdAuctionService::JoinInterestGroupCallback callback) {
url::Origin interest_group_owner = group.owner;
permissions_checker_.CheckPermissions(
InterestGroupPermissionsChecker::Operation::kJoin, frame_origin,
interest_group_owner, network_isolation_key, url_loader_factory,
base::BindOnce(
&InterestGroupManagerImpl::OnJoinInterestGroupPermissionsChecked,
base::Unretained(this), std::move(group), joining_url,
report_result_only, std::move(callback)));
}
void InterestGroupManagerImpl::CheckPermissionsAndLeaveInterestGroup(
const blink::InterestGroupKey& group_key,
const url::Origin& main_frame,
const url::Origin& frame_origin,
const net::NetworkIsolationKey& network_isolation_key,
bool report_result_only,
network::mojom::URLLoaderFactory& url_loader_factory,
blink::mojom::AdAuctionService::LeaveInterestGroupCallback callback) {
permissions_checker_.CheckPermissions(
InterestGroupPermissionsChecker::Operation::kLeave, frame_origin,
group_key.owner, network_isolation_key, url_loader_factory,
base::BindOnce(
&InterestGroupManagerImpl::OnLeaveInterestGroupPermissionsChecked,
base::Unretained(this), group_key, main_frame, report_result_only,
std::move(callback)));
}
void InterestGroupManagerImpl::JoinInterestGroup(blink::InterestGroup group,
const GURL& joining_url) {
NotifyInterestGroupAccessed(InterestGroupObserver::kJoin, group.owner,
group.name);
blink::InterestGroupKey group_key(group.owner, group.name);
impl_.AsyncCall(&InterestGroupStorage::JoinInterestGroup)
.WithArgs(std::move(group), std::move(joining_url));
// This needs to happen second so that the DB row is created.
GetInterestGroup(
group_key,
base::BindOnce(
&InterestGroupManagerImpl::
QueueKAnonymityUpdateForInterestGroupFromJoinInterestGroup,
weak_factory_.GetWeakPtr()));
}
void InterestGroupManagerImpl::LeaveInterestGroup(
const blink::InterestGroupKey& group_key,
const ::url::Origin& main_frame) {
NotifyInterestGroupAccessed(InterestGroupObserver::kLeave, group_key.owner,
group_key.name);
impl_.AsyncCall(&InterestGroupStorage::LeaveInterestGroup)
.WithArgs(group_key, main_frame);
}
void InterestGroupManagerImpl::UpdateInterestGroupsOfOwner(
const url::Origin& owner,
network::mojom::ClientSecurityStatePtr client_security_state) {
update_manager_.UpdateInterestGroupsOfOwner(owner,
std::move(client_security_state));
}
void InterestGroupManagerImpl::UpdateInterestGroupsOfOwners(
base::span<url::Origin> owners,
network::mojom::ClientSecurityStatePtr client_security_state) {
update_manager_.UpdateInterestGroupsOfOwners(
owners, std::move(client_security_state));
}
void InterestGroupManagerImpl::RecordInterestGroupBids(
const blink::InterestGroupSet& group_keys) {
if (group_keys.empty()) {
return;
}
for (const auto& group_key : group_keys) {
NotifyInterestGroupAccessed(InterestGroupObserver::kBid, group_key.owner,
group_key.name);
}
impl_.AsyncCall(&InterestGroupStorage::RecordInterestGroupBids)
.WithArgs(group_keys);
}
void InterestGroupManagerImpl::RecordInterestGroupWin(
const blink::InterestGroupKey& group_key,
const std::string& ad_json) {
NotifyInterestGroupAccessed(InterestGroupObserver::kWin, group_key.owner,
group_key.name);
impl_.AsyncCall(&InterestGroupStorage::RecordInterestGroupWin)
.WithArgs(group_key, std::move(ad_json));
}
void InterestGroupManagerImpl::RegisterAdKeysAsJoined(
base::flat_set<std::string> keys) {
k_anonymity_manager_->RegisterAdKeysAsJoined(std::move(keys));
}
void InterestGroupManagerImpl::GetInterestGroup(
const url::Origin& owner,
const std::string& name,
base::OnceCallback<void(absl::optional<StorageInterestGroup>)> callback) {
GetInterestGroup(blink::InterestGroupKey(owner, name), std::move(callback));
}
void InterestGroupManagerImpl::GetInterestGroup(
const blink::InterestGroupKey& group_key,
base::OnceCallback<void(absl::optional<StorageInterestGroup>)> callback) {
impl_.AsyncCall(&InterestGroupStorage::GetInterestGroup)
.WithArgs(group_key)
.Then(std::move(callback));
}
void InterestGroupManagerImpl::GetAllInterestGroupOwners(
base::OnceCallback<void(std::vector<url::Origin>)> callback) {
impl_.AsyncCall(&InterestGroupStorage::GetAllInterestGroupOwners)
.Then(std::move(callback));
}
void InterestGroupManagerImpl::GetInterestGroupsForOwner(
const url::Origin& owner,
base::OnceCallback<void(std::vector<StorageInterestGroup>)> callback) {
impl_.AsyncCall(&InterestGroupStorage::GetInterestGroupsForOwner)
.WithArgs(owner)
.Then(
base::BindOnce(&InterestGroupManagerImpl::OnGetInterestGroupsComplete,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void InterestGroupManagerImpl::DeleteInterestGroupData(
StoragePartition::StorageKeyMatcherFunction storage_key_matcher,
base::OnceClosure completion_callback) {
impl_.AsyncCall(&InterestGroupStorage::DeleteInterestGroupData)
.WithArgs(std::move(storage_key_matcher))
.Then(std::move(completion_callback));
}
void InterestGroupManagerImpl::DeleteAllInterestGroupData(
base::OnceClosure completion_callback) {
impl_.AsyncCall(&InterestGroupStorage::DeleteAllInterestGroupData)
.Then(std::move(completion_callback));
}
void InterestGroupManagerImpl::GetLastMaintenanceTimeForTesting(
base::RepeatingCallback<void(base::Time)> callback) const {
impl_.AsyncCall(&InterestGroupStorage::GetLastMaintenanceTimeForTesting)
.Then(std::move(callback));
}
void InterestGroupManagerImpl::EnqueueReports(
ReportType report_type,
std::vector<GURL> report_urls,
const url::Origin& frame_origin,
const network::mojom::ClientSecurityState& client_security_state,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
if (report_urls.empty()) {
return;
}
// For memory usage reasons, purge the queue if it has at least
// `max_report_queue_length_` entries at the time we're about to add new
// entries.
if (report_requests_.size() >=
static_cast<unsigned int>(max_report_queue_length_)) {
report_requests_.clear();
}
const char* report_type_name;
switch (report_type) {
case ReportType::kSendReportTo:
report_type_name = "SendReportToReport";
break;
case ReportType::kDebugWin:
report_type_name = "DebugWinReport";
break;
case ReportType::kDebugLoss:
report_type_name = "DebugLossReport";
break;
}
for (GURL& report_url : report_urls) {
auto report_request = std::make_unique<ReportRequest>();
report_request->request_url_size_bytes = report_url.spec().size();
report_request->simple_url_loader = BuildSimpleUrlLoader(
std::move(report_url), frame_origin, client_security_state);
report_request->name = report_type_name;
report_request->url_loader_factory = url_loader_factory;
report_requests_.emplace_back(std::move(report_request));
}
while (!report_requests_.empty() &&
num_active_ < max_active_report_requests_) {
++num_active_;
TrySendingOneReport();
}
}
void InterestGroupManagerImpl::SetInterestGroupPriority(
const blink::InterestGroupKey& group_key,
double priority) {
impl_.AsyncCall(&InterestGroupStorage::SetInterestGroupPriority)
.WithArgs(group_key, priority);
}
void InterestGroupManagerImpl::UpdateInterestGroupPriorityOverrides(
const blink::InterestGroupKey& group_key,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides) {
impl_.AsyncCall(&InterestGroupStorage::UpdateInterestGroupPriorityOverrides)
.WithArgs(group_key, std::move(update_priority_signals_overrides));
}
void InterestGroupManagerImpl::ClearPermissionsCache() {
permissions_checker_.ClearCache();
}
void InterestGroupManagerImpl::QueueKAnonymityUpdateForInterestGroup(
const StorageInterestGroup& group) {
k_anonymity_manager_->QueryKAnonymityForInterestGroup(group);
}
void InterestGroupManagerImpl::UpdateKAnonymity(
const StorageInterestGroup::KAnonymityData& data) {
impl_.AsyncCall(&InterestGroupStorage::UpdateKAnonymity).WithArgs(data);
}
void InterestGroupManagerImpl::GetLastKAnonymityReported(
const std::string& key,
base::OnceCallback<void(absl::optional<base::Time>)> callback) {
impl_.AsyncCall(&InterestGroupStorage::GetLastKAnonymityReported)
.WithArgs(key)
.Then(std::move(callback));
}
void InterestGroupManagerImpl::UpdateLastKAnonymityReported(
const std::string& key) {
impl_.AsyncCall(&InterestGroupStorage::UpdateLastKAnonymityReported)
.WithArgs(key);
}
void InterestGroupManagerImpl::GetInterestGroupAdAuctionData(
url::Origin top_level_origin,
base::Uuid generation_id,
base::OnceCallback<void(BiddingAndAuctionData)> callback) {
AdAuctionDataLoaderState state;
state.serializer.SetPublisher(top_level_origin.Serialize());
state.serializer.SetGenerationId(std::move(generation_id));
state.callback = std::move(callback);
GetAllInterestGroupOwners(base::BindOnce(
&InterestGroupManagerImpl::LoadNextInterestGroupAdAuctionData,
weak_factory_.GetWeakPtr(), std::move(state)));
}
void InterestGroupManagerImpl::LoadNextInterestGroupAdAuctionData(
AdAuctionDataLoaderState state,
std::vector<url::Origin> owners) {
if (!owners.empty()) {
url::Origin next_owner = std::move(owners.back());
owners.pop_back();
GetInterestGroupsForOwner(
next_owner,
base::BindOnce(
&InterestGroupManagerImpl::OnLoadedNextInterestGroupAdAuctionData,
weak_factory_.GetWeakPtr(), std::move(state), std::move(owners),
next_owner));
return;
}
// Loading is finished.
OnAdAuctionDataLoadComplete(std::move(state));
}
void InterestGroupManagerImpl::OnLoadedNextInterestGroupAdAuctionData(
AdAuctionDataLoaderState state,
std::vector<url::Origin> owners,
url::Origin owner,
std::vector<StorageInterestGroup> groups) {
state.serializer.AddGroups(std::move(owner), std::move(groups));
LoadNextInterestGroupAdAuctionData(std::move(state), std::move(owners));
}
void InterestGroupManagerImpl::OnAdAuctionDataLoadComplete(
AdAuctionDataLoaderState state) {
std::move(state.callback).Run(state.serializer.Build());
}
void InterestGroupManagerImpl::GetBiddingAndAuctionServerKey(
network::mojom::URLLoaderFactory* loader,
base::OnceCallback<void(absl::optional<BiddingAndAuctionServerKey>)>
callback) {
ba_key_fetcher_.GetOrFetchKey(loader, std::move(callback));
}
void InterestGroupManagerImpl::OnJoinInterestGroupPermissionsChecked(
blink::InterestGroup group,
const GURL& joining_url,
bool report_result_only,
blink::mojom::AdAuctionService::JoinInterestGroupCallback callback,
bool can_join) {
// Invoke callback before calling JoinInterestGroup(), which posts a task to
// another thread. Any FLEDGE call made from the renderer will need to pass
// through the UI thread and then bounce over the database thread, so it will
// see the new InterestGroup, so it's not necessary to actually wait for the
// database to be updated before invoking the callback. Waiting before
// invoking the callback may potentially leak whether the user was previously
// in the InterestGroup through timing differences.
std::move(callback).Run(/*failed_well_known_check=*/!can_join);
if (!report_result_only && can_join)
JoinInterestGroup(std::move(group), joining_url);
}
void InterestGroupManagerImpl::OnLeaveInterestGroupPermissionsChecked(
const blink::InterestGroupKey& group_key,
const url::Origin& main_frame,
bool report_result_only,
blink::mojom::AdAuctionService::LeaveInterestGroupCallback callback,
bool can_leave) {
// Invoke callback before calling LeaveInterestGroup(), which posts a task to
// another thread. Any FLEDGE call made from the renderer will need to pass
// through the UI thread and then bounce over the database thread, so it will
// see the new InterestGroup, so it's not necessary to actually wait for the
// database to be updated before invoking the callback. Waiting before
// invoking the callback may potentially leak whether the user was previously
// in the InterestGroup through timing differences.
std::move(callback).Run(/*failed_well_known_check=*/!can_leave);
if (!report_result_only && can_leave)
LeaveInterestGroup(group_key, main_frame);
}
void InterestGroupManagerImpl::GetInterestGroupsForUpdate(
const url::Origin& owner,
int groups_limit,
base::OnceCallback<void(std::vector<StorageInterestGroup>)> callback) {
impl_.AsyncCall(&InterestGroupStorage::GetInterestGroupsForUpdate)
.WithArgs(owner, groups_limit)
.Then(std::move(callback));
}
void InterestGroupManagerImpl::UpdateInterestGroup(
const blink::InterestGroupKey& group_key,
InterestGroupUpdate update,
base::OnceCallback<void(bool)> callback) {
NotifyInterestGroupAccessed(InterestGroupObserver::kUpdate, group_key.owner,
group_key.name);
impl_.AsyncCall(&InterestGroupStorage::UpdateInterestGroup)
.WithArgs(group_key, std::move(update))
.Then(std::move(callback));
}
void InterestGroupManagerImpl::ReportUpdateFailed(
const blink::InterestGroupKey& group_key,
bool parse_failure) {
impl_.AsyncCall(&InterestGroupStorage::ReportUpdateFailed)
.WithArgs(group_key, parse_failure);
}
void InterestGroupManagerImpl::OnGetInterestGroupsComplete(
base::OnceCallback<void(std::vector<StorageInterestGroup>)> callback,
std::vector<StorageInterestGroup> groups) {
for (const auto& group : groups) {
NotifyInterestGroupAccessed(InterestGroupObserver::kLoaded,
group.interest_group.owner,
group.interest_group.name);
}
std::move(callback).Run(std::move(groups));
}
void InterestGroupManagerImpl::NotifyInterestGroupAccessed(
InterestGroupObserver::AccessType type,
const url::Origin& owner_origin,
const std::string& name) {
// Don't bother getting the time if there are no observers.
if (observers_.empty())
return;
base::Time now = base::Time::Now();
for (InterestGroupObserver& observer : observers_) {
observer.OnInterestGroupAccessed(now, type, owner_origin, name);
}
}
void InterestGroupManagerImpl::TrySendingOneReport() {
DCHECK_GT(num_active_, 0);
if (report_requests_.empty()) {
--num_active_;
if (num_active_ == 0)
timeout_timer_.Stop();
return;
}
if (!timeout_timer_.IsRunning()) {
timeout_timer_.Start(
FROM_HERE, max_reporting_round_duration_,
base::BindOnce(&InterestGroupManagerImpl::TimeoutReports,
base::Unretained(this)));
}
std::unique_ptr<ReportRequest> report_request =
std::move(report_requests_.front());
report_requests_.pop_front();
base::UmaHistogramCounts100000(
base::StrCat(
{"Ads.InterestGroup.Net.RequestUrlSizeBytes.", report_request->name}),
report_request->request_url_size_bytes);
base::UmaHistogramCounts100(
base::StrCat(
{"Ads.InterestGroup.Net.ResponseSizeBytes.", report_request->name}),
0);
network::SimpleURLLoader* simple_url_loader_ptr =
report_request->simple_url_loader.get();
// Pass simple_url_loader to keep it alive until the request fails or succeeds
// to prevent cancelling the request.
simple_url_loader_ptr->DownloadHeadersOnly(
report_request->url_loader_factory.get(),
base::BindOnce(&InterestGroupManagerImpl::OnOneReportSent,
weak_factory_.GetWeakPtr(),
std::move(report_request->simple_url_loader)));
}
void InterestGroupManagerImpl::OnOneReportSent(
std::unique_ptr<network::SimpleURLLoader> simple_url_loader,
scoped_refptr<net::HttpResponseHeaders> response_headers) {
DCHECK_GT(num_active_, 0);
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&InterestGroupManagerImpl::TrySendingOneReport,
weak_factory_.GetWeakPtr()),
reporting_interval_);
}
void InterestGroupManagerImpl::TimeoutReports() {
// TODO(qingxinwu): maybe add UMA metrics to learn how often this happens.
report_requests_.clear();
}
void InterestGroupManagerImpl::
QueueKAnonymityUpdateForInterestGroupFromJoinInterestGroup(
absl::optional<StorageInterestGroup> maybe_group) {
// We just joined the group, so it must exist.
// We don't need to worry about the DB size limit, since older groups
// are removed first.
DCHECK(maybe_group);
if (maybe_group)
QueueKAnonymityUpdateForInterestGroup(*maybe_group);
}
} // namespace content