blob: b8fd75c1a2fcb662eca013b3d4bc64f1b63fbc8e [file] [log] [blame]
// Copyright 2022 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/private_aggregation/private_aggregation_manager_impl.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <map>
#include <memory>
#include <numeric>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/checked_math.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/task/updateable_sequenced_task_runner.h"
#include "base/time/time.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/aggregation_service/aggregation_service.h"
#include "content/browser/private_aggregation/private_aggregation_budget_key.h"
#include "content/browser/private_aggregation/private_aggregation_budgeter.h"
#include "content/browser/private_aggregation/private_aggregation_caller_api.h"
#include "content/browser/private_aggregation/private_aggregation_host.h"
#include "content/browser/private_aggregation/private_aggregation_pending_contributions.h"
#include "content/browser/private_aggregation/private_aggregation_utils.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/private_aggregation_data_model.h"
#include "content/public/browser/storage_partition.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h"
#include "third_party/blink/public/mojom/private_aggregation/private_aggregation_host.mojom.h"
#include "url/origin.h"
namespace content {
namespace {
void RecordBudgeterResultHistogram(
PrivateAggregationBudgeter::RequestResult request_result) {
base::UmaHistogramEnumeration(
"PrivacySandbox.PrivateAggregation.Budgeter.RequestResult3",
request_result);
}
void RecordManagerResultHistogram(
PrivateAggregationManagerImpl::RequestResult request_result) {
base::UmaHistogramEnumeration(
"PrivacySandbox.PrivateAggregation.Manager.RequestResult",
request_result);
}
} // namespace
struct PrivateAggregationManagerImpl::InProgressBudgetRequest {
PrivateAggregationHost::ReportRequestGenerator report_request_generator;
PrivateAggregationPendingContributions pending_contributions;
const PrivateAggregationBudgetKey budget_key;
const PrivateAggregationHost::NullReportBehavior null_report_behavior;
std::optional<PrivateAggregationBudgeter::RequestResult>
inspect_request_result;
};
PrivateAggregationManagerImpl::PrivateAggregationManagerImpl(
bool exclusively_run_in_memory,
const base::FilePath& user_data_directory,
StoragePartitionImpl* storage_partition)
: PrivateAggregationManagerImpl(
std::make_unique<PrivateAggregationBudgeter>(
// This uses BLOCK_SHUTDOWN as some data deletion operations may
// be running when the browser is closed, and we want to ensure
// all data is deleted correctly. Additionally, we use
// MUST_USE_FOREGROUND to avoid priority inversions if a task is
// already running when the priority is increased.
base::ThreadPool::CreateUpdateableSequencedTaskRunner(
base::TaskTraits(base::TaskPriority::BEST_EFFORT,
base::MayBlock(),
base::TaskShutdownBehavior::BLOCK_SHUTDOWN,
base::ThreadPolicy::MUST_USE_FOREGROUND)),
exclusively_run_in_memory,
/*path_to_db_dir=*/user_data_directory),
std::make_unique<PrivateAggregationHost>(
/*on_report_request_details_received=*/base::BindRepeating(
&PrivateAggregationManagerImpl::
OnReportRequestDetailsReceivedFromHost,
base::Unretained(this)),
storage_partition ? storage_partition->browser_context()
: nullptr),
storage_partition) {}
PrivateAggregationManagerImpl::PrivateAggregationManagerImpl(
std::unique_ptr<PrivateAggregationBudgeter> budgeter,
std::unique_ptr<PrivateAggregationHost> host,
StoragePartitionImpl* storage_partition)
: budgeter_(std::move(budgeter)),
host_(std::move(host)),
storage_partition_(storage_partition) {
CHECK(budgeter_);
CHECK(host_);
}
PrivateAggregationManagerImpl::~PrivateAggregationManagerImpl() = default;
bool PrivateAggregationManagerImpl::BindNewReceiver(
url::Origin worklet_origin,
url::Origin top_frame_origin,
PrivateAggregationCallerApi caller_api,
std::optional<std::string> context_id,
std::optional<base::TimeDelta> timeout,
std::optional<url::Origin> aggregation_coordinator_origin,
size_t filtering_id_max_bytes,
std::optional<size_t> max_contributions,
mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>
pending_receiver) {
return host_->BindNewReceiver(
std::move(worklet_origin), std::move(top_frame_origin), caller_api,
std::move(context_id), std::move(timeout),
std::move(aggregation_coordinator_origin), filtering_id_max_bytes,
std::move(max_contributions), std::move(pending_receiver));
}
void PrivateAggregationManagerImpl::ClearBudgetData(
base::Time delete_begin,
base::Time delete_end,
StoragePartition::StorageKeyMatcherFunction filter,
base::OnceClosure done) {
budgeter_->ClearData(delete_begin, delete_end, std::move(filter),
std::move(done));
}
bool PrivateAggregationManagerImpl::IsDebugModeAllowed(
const url::Origin& top_frame_origin,
const url::Origin& reporting_origin) {
return host_->IsDebugModeAllowed(top_frame_origin, reporting_origin);
}
void PrivateAggregationManagerImpl::OnReportRequestDetailsReceivedFromHost(
PrivateAggregationHost::ReportRequestGenerator report_request_generator,
PrivateAggregationPendingContributions::Wrapper contributions_wrapper,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior null_report_behavior) {
if (base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting)) {
if (contributions_wrapper.GetPendingContributions().IsEmpty()) {
// An empty non-deterministic report should've been dropped already.
CHECK_EQ(null_report_behavior,
PrivateAggregationHost::NullReportBehavior::kSendNullReport);
RecordManagerResultHistogram(RequestResult::kSentWithoutContributions);
OnContributionsFinalized(std::move(report_request_generator),
/*contributions=*/{}, budget_key.caller_api());
return;
}
BudgetRequestId budget_request_id =
BudgetRequestId(num_requests_processed_++);
auto emplace_pair = in_progress_budget_requests_.emplace(
budget_request_id,
InProgressBudgetRequest{
.report_request_generator = std::move(report_request_generator),
.pending_contributions =
std::move(contributions_wrapper.GetPendingContributions()),
.budget_key = std::move(budget_key),
.null_report_behavior = null_report_behavior});
InProgressBudgetRequest& current_request = emplace_pair.first->second;
budgeter_->InspectBudgetAndLock(
current_request.pending_contributions.unconditional_contributions(),
current_request.budget_key, /*result_callback=*/
// Unretained is safe as the `budgeter_` is owned by `this`.
base::BindOnce(
&PrivateAggregationManagerImpl::OnTestBudgetAndLockReturned,
base::Unretained(this), budget_request_id));
return;
}
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions = std::move(contributions_wrapper.GetContributionsVector());
base::CheckedNumeric<int> budget_needed = std::accumulate(
contributions.begin(), contributions.end(),
/*init=*/base::CheckedNumeric<int>(0), /*op=*/
[](base::CheckedNumeric<int> running_sum,
const blink::mojom::AggregatableReportHistogramContribution&
contribution) { return running_sum + contribution.value; });
PrivateAggregationCallerApi caller_api = budget_key.caller_api();
if (!budget_needed.IsValid()) {
OnConsumeBudgetReturned(std::move(report_request_generator),
std::move(contributions), caller_api,
null_report_behavior,
PrivateAggregationBudgeter::RequestResult::
kRequestedMoreThanTotalBudget);
return;
}
// No need to request budget if none is needed.
if (budget_needed.ValueOrDie() == 0) {
RecordManagerResultHistogram(RequestResult::kSentWithoutContributions);
CHECK(contributions.empty());
OnContributionsFinalized(std::move(report_request_generator),
std::move(contributions), caller_api);
return;
}
CHECK(!contributions.empty());
int minimum_value_for_metrics =
std::ranges::min(
contributions, /*comp=*/{}, /*proj=*/
&blink::mojom::AggregatableReportHistogramContribution::value)
.value;
budgeter_->ConsumeBudget(
budget_needed.ValueOrDie(), std::move(budget_key),
minimum_value_for_metrics, /*on_done=*/
// Unretained is safe as the `budgeter_` is owned by `this`.
base::BindOnce(
&PrivateAggregationManagerImpl::OnConsumeBudgetReturned,
base::Unretained(this), std::move(report_request_generator),
std::move(contributions), caller_api, null_report_behavior));
}
AggregationService* PrivateAggregationManagerImpl::GetAggregationService() {
CHECK(storage_partition_);
return AggregationService::GetService(storage_partition_->browser_context());
}
void PrivateAggregationManagerImpl::OnTestBudgetAndLockReturned(
BudgetRequestId budget_request_id,
PrivateAggregationBudgeter::InspectBudgetCallResult result) {
CHECK(base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
CHECK(in_progress_budget_requests_.contains(budget_request_id));
InProgressBudgetRequest& current_request =
in_progress_budget_requests_.at(budget_request_id);
current_request.inspect_request_result = result.query_result.overall_result;
CHECK(current_request.inspect_request_result.has_value());
PrivateAggregationPendingContributions::NullReportBehavior
pending_contributions_null_report_behavior =
current_request.null_report_behavior ==
PrivateAggregationHost::NullReportBehavior::kSendNullReport
? PrivateAggregationPendingContributions::NullReportBehavior::
kSendNullReport
: PrivateAggregationPendingContributions::NullReportBehavior::
kDontSendReport;
const std::vector<blink::mojom::AggregatableReportHistogramContribution>&
final_unmerged_contributions =
current_request.pending_contributions
.CompileFinalUnmergedContributions(
std::move(result.query_result.result_for_each_contribution),
result.pending_report_limit_result,
pending_contributions_null_report_behavior);
if (!result.lock.has_value()) {
// A fatal error occurred, generate a fictitious budget query result denying
// the conditional contributions (if present).
result.query_result.result_for_each_contribution =
std::vector<PrivateAggregationBudgeter::ResultForContribution>(
final_unmerged_contributions.size(),
PrivateAggregationBudgeter::ResultForContribution::kDenied);
OnConsumeBudgetWithLockReturned(budget_request_id,
std::move(result.query_result));
return;
}
budgeter_->ConsumeBudget(
std::move(result.lock).value(), final_unmerged_contributions,
current_request.budget_key, /*result_callback=*/
// Unretained is safe as the `budgeter_` is owned by `this`.
base::BindOnce(
&PrivateAggregationManagerImpl::OnConsumeBudgetWithLockReturned,
base::Unretained(this), budget_request_id));
}
void PrivateAggregationManagerImpl::OnConsumeBudgetWithLockReturned(
BudgetRequestId budget_request_id,
PrivateAggregationBudgeter::BudgetQueryResult result) {
CHECK(base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
CHECK(in_progress_budget_requests_.contains(budget_request_id));
InProgressBudgetRequest current_request = std::move(
in_progress_budget_requests_.extract(budget_request_id).mapped());
PrivateAggregationBudgeter::RequestResult overall_result_both_phases =
PrivateAggregationBudgeter::CombineRequestResults(
/*inspect_budget_result=*/current_request.inspect_request_result
.value(),
/*consume_budget_result=*/result.overall_result);
RecordBudgeterResultHistogram(overall_result_both_phases);
std::vector<blink::mojom::AggregatableReportHistogramContribution>
final_contributions = std::move(current_request.pending_contributions)
.TakeFinalContributions(std::move(
result.result_for_each_contribution));
if (final_contributions.empty()) {
switch (current_request.null_report_behavior) {
case PrivateAggregationHost::NullReportBehavior::kDontSendReport:
RecordManagerResultHistogram(RequestResult::kNotSent);
return;
case PrivateAggregationHost::NullReportBehavior::kSendNullReport:
// This distinguishes from the case where we have an empty report due to
// untriggered conditional contributions (and no other contributions).
RecordManagerResultHistogram(
overall_result_both_phases !=
PrivateAggregationBudgeter::RequestResult::kApproved
? RequestResult::kSentButContributionsClearedDueToBudgetDenial
: RequestResult::kSentWithoutContributions);
break;
}
} else {
RecordManagerResultHistogram(RequestResult::kSentWithContributions);
}
OnContributionsFinalized(std::move(current_request.report_request_generator),
std::move(final_contributions),
current_request.budget_key.caller_api());
}
void PrivateAggregationManagerImpl::OnConsumeBudgetReturned(
PrivateAggregationHost::ReportRequestGenerator report_request_generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationCallerApi caller_api,
PrivateAggregationHost::NullReportBehavior null_report_behavior,
PrivateAggregationBudgeter::RequestResult request_result) {
CHECK(!base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
RecordBudgeterResultHistogram(request_result);
// TODO(crbug.com/355271550): Consider allowing a subset of contributions to
// be sent if there's insufficient budget for them all.
if (request_result == PrivateAggregationBudgeter::RequestResult::kApproved) {
CHECK(!contributions.empty());
RecordManagerResultHistogram(RequestResult::kSentWithContributions);
} else {
switch (null_report_behavior) {
case PrivateAggregationHost::NullReportBehavior::kDontSendReport:
RecordManagerResultHistogram(RequestResult::kNotSent);
return;
case PrivateAggregationHost::NullReportBehavior::kSendNullReport:
RecordManagerResultHistogram(
RequestResult::kSentButContributionsClearedDueToBudgetDenial);
contributions.clear();
break;
}
}
OnContributionsFinalized(std::move(report_request_generator),
std::move(contributions), caller_api);
}
void PrivateAggregationManagerImpl::OnContributionsFinalized(
PrivateAggregationHost::ReportRequestGenerator report_request_generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationCallerApi caller_api) {
AggregationService* aggregation_service = GetAggregationService();
if (!aggregation_service) {
return;
}
AggregatableReportRequest report_request =
std::move(report_request_generator).Run(std::move(contributions));
// If the request has debug mode enabled, immediately send a duplicate of the
// requested report to a special debug reporting endpoint.
if (report_request.shared_info().debug_mode ==
AggregatableReportSharedInfo::DebugMode::kEnabled) {
std::string immediate_debug_reporting_path =
private_aggregation::GetReportingPath(
caller_api,
/*is_immediate_debug_report=*/true);
std::optional<AggregatableReportRequest> debug_request =
AggregatableReportRequest::Create(
report_request.payload_contents(),
report_request.shared_info().Clone(),
AggregatableReportRequest::DelayType::Unscheduled,
std::move(immediate_debug_reporting_path),
report_request.debug_key(), report_request.additional_fields());
CHECK(debug_request.has_value());
aggregation_service->AssembleAndSendReport(
std::move(debug_request.value()));
}
aggregation_service->ScheduleReport(std::move(report_request));
}
void PrivateAggregationManagerImpl::GetAllDataKeys(
base::OnceCallback<void(std::set<DataKey>)> callback) {
budgeter_->GetAllDataKeys(base::BindOnce(
&PrivateAggregationManagerImpl::OnBudgeterGetAllDataKeysReturned,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void PrivateAggregationManagerImpl::OnBudgeterGetAllDataKeysReturned(
base::OnceCallback<void(std::set<DataKey>)> callback,
std::set<DataKey> all_keys) {
AggregationService* aggregation_service = GetAggregationService();
if (!aggregation_service) {
std::move(callback).Run(std::move(all_keys));
return;
}
aggregation_service->GetPendingReportReportingOrigins(base::BindOnce(
[](base::OnceCallback<void(std::set<DataKey>)> callback,
std::set<DataKey> all_keys, std::set<url::Origin> pending_origins) {
std::ranges::transform(
std::make_move_iterator(pending_origins.begin()),
std::make_move_iterator(pending_origins.end()),
std::inserter(all_keys, all_keys.begin()), [](url::Origin elem) {
return PrivateAggregationDataModel::DataKey(std::move(elem));
});
std::move(callback).Run(std::move(all_keys));
},
std::move(callback), std::move(all_keys)));
}
void PrivateAggregationManagerImpl::RemovePendingDataKey(
const DataKey& data_key,
base::OnceClosure callback) {
base::RepeatingClosure barrier = base::BarrierClosure(2, std::move(callback));
budgeter_->DeleteByDataKey(data_key, barrier);
AggregationService* aggregation_service = GetAggregationService();
if (!aggregation_service) {
std::move(barrier).Run();
return;
}
aggregation_service->ClearData(
/*delete_begin=*/base::Time::Min(), /*delete_end=*/base::Time::Max(),
/*filter=*/
base::BindRepeating(
std::equal_to<blink::StorageKey>(),
blink::StorageKey::CreateFirstParty(data_key.reporting_origin())),
/*done=*/std::move(barrier));
}
} // namespace content