From 5ccf828ddcb107583a4d255e5555ccb79aeda8ac Mon Sep 17 00:00:00 2001 From: Alex Turner Date: Thu, 25 Aug 2022 17:52:05 +0000 Subject: [PATCH] Implement data clearing for Private Aggregation API budgeting Ensures budgeting data is cleared when a user initiates data clearing. However, note that site-initiated data clearing does not affect budgeting data. This also makes a few drive-by changes to other data removal constants. Bug: 1328439 Change-Id: I6bd8aaa2b2e0d005317ce6bba1a498d14da5b408 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3825990 Reviewed-by: Austin Sullivan Reviewed-by: John Delaney Reviewed-by: Theodore Olsauskas-Warren Commit-Queue: Alex Turner Reviewed-by: John Abd-El-Malek Reviewed-by: Christian Dullweber Cr-Commit-Position: refs/heads/main@{#1039323} --- .../browsing_data_history_observer_service.cc | 4 +- ..._data_history_observer_service_unittest.cc | 7 +- .../chrome_browsing_data_remover_constants.h | 3 +- .../privacy_sandbox_service.cc | 5 +- .../privacy_sandbox_service_unittest.cc | 5 +- .../browsing_data_remover_impl.cc | 4 + .../browsing_data_remover_impl_unittest.cc | 9 + .../clear_site_data_handler_browsertest.cc | 2 +- .../browsing_data/clear_site_data_utils.cc | 3 +- .../private_aggregation_budget_key.h | 2 + .../private_aggregation_budgeter.cc | 126 +++- .../private_aggregation_budgeter.h | 30 +- .../private_aggregation_budgeter_unittest.cc | 561 +++++++++++++++++- .../private_aggregation_manager.h | 18 + .../private_aggregation_manager_impl.cc | 12 + .../private_aggregation_manager_impl.h | 7 + ...ivate_aggregation_manager_impl_unittest.cc | 45 ++ .../private_aggregation_test_utils.cc | 3 + .../private_aggregation_test_utils.h | 38 +- content/browser/storage_partition_impl.cc | 22 +- content/browser/storage_partition_impl.h | 9 +- .../storage_partition_impl_unittest.cc | 111 ++++ .../public/browser/browsing_data_remover.h | 17 +- content/public/browser/storage_partition.h | 1 + weblayer/browser/profile_impl.cc | 2 + 25 files changed, 1003 insertions(+), 43 deletions(-) diff --git a/chrome/browser/browsing_data/browsing_data_history_observer_service.cc b/chrome/browser/browsing_data/browsing_data_history_observer_service.cc index 9e7540fb6887e3..bc121365247f93 100644 --- a/chrome/browser/browsing_data/browsing_data_history_observer_service.cc +++ b/chrome/browser/browsing_data/browsing_data_history_observer_service.cc @@ -139,10 +139,12 @@ void DeleteStoragePartitionDataWithFilter( : base::NullCallback(); const uint32_t removal_mask = + content::StoragePartition::REMOVE_DATA_MASK_AGGREGATION_SERVICE | content::StoragePartition:: REMOVE_DATA_MASK_ATTRIBUTION_REPORTING_SITE_CREATED | content::StoragePartition:: - REMOVE_DATA_MASK_ATTRIBUTION_REPORTING_INTERNAL; + REMOVE_DATA_MASK_ATTRIBUTION_REPORTING_INTERNAL | + content::StoragePartition::REMOVE_DATA_MASK_PRIVATE_AGGREGATION_INTERNAL; const uint32_t quota_removal_mask = 0; storage_partition->ClearData( removal_mask, quota_removal_mask, std::move(storage_key_matcher), diff --git a/chrome/browser/browsing_data/browsing_data_history_observer_service_unittest.cc b/chrome/browser/browsing_data/browsing_data_history_observer_service_unittest.cc index f040889ef4f5cd..76e72d71b47b93 100644 --- a/chrome/browser/browsing_data/browsing_data_history_observer_service_unittest.cc +++ b/chrome/browser/browsing_data/browsing_data_history_observer_service_unittest.cc @@ -82,10 +82,13 @@ TEST_F(BrowsingDataHistoryObserverServiceTest, AllHistoryDeleted_DataCleared) { const absl::optional& removal_data = partition.GetRemovalData(); EXPECT_TRUE(removal_data.has_value()); - EXPECT_EQ(content::StoragePartition:: + EXPECT_EQ(content::StoragePartition::REMOVE_DATA_MASK_AGGREGATION_SERVICE | + content::StoragePartition:: REMOVE_DATA_MASK_ATTRIBUTION_REPORTING_SITE_CREATED | content::StoragePartition:: - REMOVE_DATA_MASK_ATTRIBUTION_REPORTING_INTERNAL, + REMOVE_DATA_MASK_ATTRIBUTION_REPORTING_INTERNAL | + content::StoragePartition:: + REMOVE_DATA_MASK_PRIVATE_AGGREGATION_INTERNAL, removal_data->removal_mask); EXPECT_EQ(0u, removal_data->quota_storage_removal_mask); EXPECT_EQ(base::Time(), removal_data->begin); diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h b/chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h index c29358cddbdd9d..56251e775f2ba6 100644 --- a/chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h +++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h @@ -56,8 +56,7 @@ constexpr DataType DATA_TYPE_SITE_DATA = #endif DATA_TYPE_SITE_USAGE_DATA | DATA_TYPE_DURABLE_PERMISSION | DATA_TYPE_EXTERNAL_PROTOCOL_DATA | DATA_TYPE_ISOLATED_ORIGINS | - content::BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX | - content::BrowsingDataRemover::DATA_TYPE_ATTRIBUTION_REPORTING; + content::BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX; // Datatypes protected by Important Sites. constexpr DataType IMPORTANT_SITES_DATA_TYPES = diff --git a/chrome/browser/privacy_sandbox/privacy_sandbox_service.cc b/chrome/browser/privacy_sandbox/privacy_sandbox_service.cc index ffe7db0458b6fe..89cf0f21af3ca0 100644 --- a/chrome/browser/privacy_sandbox/privacy_sandbox_service.cc +++ b/chrome/browser/privacy_sandbox/privacy_sandbox_service.cc @@ -300,10 +300,7 @@ void PrivacySandboxService::OnPrivacySandboxV2PrefChanged() { if (browsing_data_remover_) { browsing_data_remover_->Remove( base::Time::Min(), base::Time::Max(), - content::BrowsingDataRemover::DATA_TYPE_INTEREST_GROUPS | - content::BrowsingDataRemover::DATA_TYPE_AGGREGATION_SERVICE | - content::BrowsingDataRemover::DATA_TYPE_ATTRIBUTION_REPORTING | - content::BrowsingDataRemover::DATA_TYPE_TRUST_TOKENS, + content::BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX, content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB); } diff --git a/chrome/browser/privacy_sandbox/privacy_sandbox_service_unittest.cc b/chrome/browser/privacy_sandbox/privacy_sandbox_service_unittest.cc index b20ab8c5f19ddf..f3978edc6f0c93 100644 --- a/chrome/browser/privacy_sandbox/privacy_sandbox_service_unittest.cc +++ b/chrome/browser/privacy_sandbox/privacy_sandbox_service_unittest.cc @@ -1286,10 +1286,7 @@ TEST_F(PrivacySandboxServiceTest, DisablingV2SandboxClearsData) { // Disabling should start a task clearing all kAPI information. EXPECT_CALL(*mock_browsing_topics_service(), ClearAllTopicsData()).Times(1); prefs()->SetBoolean(prefs::kPrivacySandboxApisEnabledV2, false); - EXPECT_EQ(content::BrowsingDataRemover::DATA_TYPE_INTEREST_GROUPS | - content::BrowsingDataRemover::DATA_TYPE_AGGREGATION_SERVICE | - content::BrowsingDataRemover::DATA_TYPE_ATTRIBUTION_REPORTING | - content::BrowsingDataRemover::DATA_TYPE_TRUST_TOKENS, + EXPECT_EQ(content::BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX, browsing_data_remover()->GetLastUsedRemovalMaskForTesting()); EXPECT_EQ(base::Time::Min(), browsing_data_remover()->GetLastUsedBeginTimeForTesting()); diff --git a/content/browser/browsing_data/browsing_data_remover_impl.cc b/content/browser/browsing_data/browsing_data_remover_impl.cc index 4fb0f7bd67f5bf..82ac565b75ee2c 100644 --- a/content/browser/browsing_data/browsing_data_remover_impl.cc +++ b/content/browser/browsing_data/browsing_data_remover_impl.cc @@ -429,6 +429,10 @@ void BrowsingDataRemoverImpl::RemoveImpl( storage_partition_remove_mask |= StoragePartition::REMOVE_DATA_MASK_AGGREGATION_SERVICE; } + if (remove_mask & DATA_TYPE_PRIVATE_AGGREGATION_INTERNAL) { + storage_partition_remove_mask |= + StoragePartition::REMOVE_DATA_MASK_PRIVATE_AGGREGATION_INTERNAL; + } if (remove_mask & DATA_TYPE_INTEREST_GROUPS) { storage_partition_remove_mask |= StoragePartition::REMOVE_DATA_MASK_INTEREST_GROUPS; diff --git a/content/browser/browsing_data/browsing_data_remover_impl_unittest.cc b/content/browser/browsing_data/browsing_data_remover_impl_unittest.cc index f09421cd601876..5311e0d54c9e9b 100644 --- a/content/browser/browsing_data/browsing_data_remover_impl_unittest.cc +++ b/content/browser/browsing_data/browsing_data_remover_impl_unittest.cc @@ -1371,6 +1371,15 @@ TEST_F(BrowsingDataRemoverImplTest, RemoveAggregationServiceData) { StoragePartition::REMOVE_DATA_MASK_AGGREGATION_SERVICE); } +TEST_F(BrowsingDataRemoverImplTest, RemovePrivateAggregationData) { + BlockUntilBrowsingDataRemoved( + base::Time(), base::Time::Max(), + BrowsingDataRemover::DATA_TYPE_PRIVATE_AGGREGATION_INTERNAL, false); + StoragePartitionRemovalData removal_data = GetStoragePartitionRemovalData(); + EXPECT_EQ(removal_data.remove_mask, + StoragePartition::REMOVE_DATA_MASK_PRIVATE_AGGREGATION_INTERNAL); +} + class MultipleTasksObserver { public: // A simple implementation of BrowsingDataRemover::Observer. diff --git a/content/browser/browsing_data/clear_site_data_handler_browsertest.cc b/content/browser/browsing_data/clear_site_data_handler_browsertest.cc index 101d1d0ff4949a..ae401688466487 100644 --- a/content/browser/browsing_data/clear_site_data_handler_browsertest.cc +++ b/content/browser/browsing_data/clear_site_data_handler_browsertest.cc @@ -106,7 +106,7 @@ class TestBrowsingDataRemoverDelegate : public MockBrowsingDataRemoverDelegate { : 0) | (cache ? BrowsingDataRemover::DATA_TYPE_CACHE : 0); data_type_mask &= - ~BrowsingDataRemover::DATA_TYPE_ATTRIBUTION_REPORTING_INTERNAL; + ~BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX_INTERNAL; BrowsingDataFilterBuilderImpl filter_builder( BrowsingDataFilterBuilder::Mode::kDelete); diff --git a/content/browser/browsing_data/clear_site_data_utils.cc b/content/browser/browsing_data/clear_site_data_utils.cc index b6c75de684f3e4..c7754d7c5c4ddc 100644 --- a/content/browser/browsing_data/clear_site_data_utils.cc +++ b/content/browser/browsing_data/clear_site_data_utils.cc @@ -98,8 +98,7 @@ class SiteDataClearer : public BrowsingDataRemover::Observer { remove_mask |= BrowsingDataRemover::DATA_TYPE_DOM_STORAGE; remove_mask |= BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX; // Internal data should not be removed by site-initiated deletions. - remove_mask &= - ~BrowsingDataRemover::DATA_TYPE_ATTRIBUTION_REPORTING_INTERNAL; + remove_mask &= ~BrowsingDataRemover::DATA_TYPE_PRIVACY_SANDBOX_INTERNAL; } if (clear_cache_) remove_mask |= BrowsingDataRemover::DATA_TYPE_CACHE; diff --git a/content/browser/private_aggregation/private_aggregation_budget_key.h b/content/browser/private_aggregation/private_aggregation_budget_key.h index 08acfc75907190..ee36a3493fc947 100644 --- a/content/browser/private_aggregation/private_aggregation_budget_key.h +++ b/content/browser/private_aggregation/private_aggregation_budget_key.h @@ -25,6 +25,8 @@ class CONTENT_EXPORT PrivateAggregationBudgetKey { // Represents a period of time for which budget usage is recorded. This // interval includes the `start_time()` instant but excludes the end time // (`start_time() + kDuration`) instant. + // TODO(crbug.com/1353473): Ensure `base::Time::Min()` and nearby times are + // handled correctly. class TimeWindow { public: static constexpr base::TimeDelta kDuration = base::Hours(1); diff --git a/content/browser/private_aggregation/private_aggregation_budgeter.cc b/content/browser/private_aggregation/private_aggregation_budgeter.cc index 6535d68b38fb0a..24065978fb96cf 100644 --- a/content/browser/private_aggregation/private_aggregation_budgeter.cc +++ b/content/browser/private_aggregation/private_aggregation_budgeter.cc @@ -25,7 +25,10 @@ #include "content/browser/private_aggregation/private_aggregation_budget_key.h" #include "content/browser/private_aggregation/private_aggregation_budget_storage.h" #include "content/browser/private_aggregation/proto/private_aggregation_budgets.pb.h" +#include "third_party/blink/public/common/storage_key/storage_key.h" #include "third_party/protobuf/src/google/protobuf/repeated_field.h" +#include "url/gurl.h" +#include "url/origin.h" namespace content { @@ -77,14 +80,13 @@ void PrivateAggregationBudgeter::ConsumeBudget( const PrivateAggregationBudgetKey& budget_key, base::OnceCallback on_done) { if (storage_status_ == StorageStatus::kInitializing) { - if (pending_consume_budget_calls_.size() >= kMaxPendingCalls) { + if (pending_calls_.size() >= kMaxPendingCalls) { std::move(on_done).Run(false); return; } - // `base::Unretained` is safe as `pending_consume_budget_calls_` is owned by - // `this`. - pending_consume_budget_calls_.push_back(base::BindOnce( + // `base::Unretained` is safe as `pending_calls_` is owned by `this`. + pending_calls_.push_back(base::BindOnce( &PrivateAggregationBudgeter::ConsumeBudgetImpl, base::Unretained(this), budget, budget_key, std::move(on_done))); } else { @@ -92,6 +94,24 @@ void PrivateAggregationBudgeter::ConsumeBudget( } } +void PrivateAggregationBudgeter::ClearData( + base::Time delete_begin, + base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done) { + if (storage_status_ == StorageStatus::kInitializing) { + // To ensure that data deletion always succeeds, we don't check + // `pending_calls.size()` here. + + // `base::Unretained` is safe as `pending_calls_` is owned by `this`. + pending_calls_.push_back(base::BindOnce( + &PrivateAggregationBudgeter::ClearDataImpl, base::Unretained(this), + delete_begin, delete_end, std::move(filter), std::move(done))); + } else { + ClearDataImpl(delete_begin, delete_end, std::move(filter), std::move(done)); + } +} + void PrivateAggregationBudgeter::OnStorageDoneInitializing( std::unique_ptr storage) { DCHECK(shutdown_initializing_storage_); @@ -110,10 +130,10 @@ void PrivateAggregationBudgeter::OnStorageDoneInitializing( } void PrivateAggregationBudgeter::ProcessAllPendingCalls() { - for (base::OnceClosure& cb : pending_consume_budget_calls_) { + for (base::OnceClosure& cb : pending_calls_) { std::move(cb).Run(); } - pending_consume_budget_calls_.clear(); + pending_calls_.clear(); } // TODO(crbug.com/1336733): Consider enumerating different error cases and log @@ -215,4 +235,98 @@ void PrivateAggregationBudgeter::ConsumeBudgetImpl( std::move(on_done).Run(budget_increase_allowed); } +void PrivateAggregationBudgeter::ClearDataImpl( + base::Time delete_begin, + base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done) { + switch (storage_status_) { + case StorageStatus::kInitializing: + NOTREACHED(); + break; + case StorageStatus::kInitializationFailed: + std::move(done).Run(); + return; + case StorageStatus::kOpen: + break; + } + + // TODO(alexmt): Delay `done` being run until after the database task is + // complete. + + // Treat null times as unbounded lower or upper range. This is used by + // browsing data remover. + if (delete_begin.is_null()) + delete_begin = base::Time::Min(); + + if (delete_end.is_null()) + delete_end = base::Time::Max(); + + bool is_all_time_covered = delete_begin.is_min() && delete_end.is_max(); + + if (is_all_time_covered && filter.is_null()) { + storage_->budgets_data()->DeleteAllData(); + std::move(done).Run(); + return; + } + + std::vector origins_to_delete; + + for (const auto& [origin_key, budgets] : + storage_->budgets_data()->GetAllCached()) { + if (filter.is_null() || + filter.Run(blink::StorageKey(url::Origin::Create(GURL(origin_key))))) { + origins_to_delete.push_back(origin_key); + } + } + + if (is_all_time_covered) { + storage_->budgets_data()->DeleteData(origins_to_delete); + std::move(done).Run(); + return; + } + + // Ensure we round down to capture any time windows that partially overlap. + int64_t serialized_delete_begin = + delete_begin.is_min() + ? SerializeTimeForStorage(base::Time::Min()) + : SerializeTimeForStorage( + PrivateAggregationBudgetKey::TimeWindow(delete_begin) + .start_time()); + + // No need to round up as we compare against the time window's start time. + int64_t serialized_delete_end = SerializeTimeForStorage(delete_end); + + for (const std::string& origin_key : origins_to_delete) { + proto::PrivateAggregationBudgets budgets; + storage_->budgets_data()->TryGetData(origin_key, &budgets); + + static constexpr PrivateAggregationBudgetKey::Api kAllApis[] = { + PrivateAggregationBudgetKey::Api::kFledge, + PrivateAggregationBudgetKey::Api::kSharedStorage}; + + for (PrivateAggregationBudgetKey::Api api : kAllApis) { + google::protobuf::RepeatedPtrField< + proto::PrivateAggregationBudgetPerHour>* hourly_budgets = + GetHourlyBudgets(api, budgets); + DCHECK(hourly_budgets); + + auto new_end = std::remove_if( + hourly_budgets->begin(), hourly_budgets->end(), + [=](const proto::PrivateAggregationBudgetPerHour& elem) { + return elem.hour_start_timestamp() >= serialized_delete_begin && + elem.hour_start_timestamp() <= serialized_delete_end; + }); + hourly_budgets->erase(new_end, hourly_budgets->end()); + } + storage_->budgets_data()->UpdateData(origin_key, budgets); + } + + // A no-op call to force the database to be flushed immediately instead of + // waiting up to `PrivateAggregationBudgetStorage::kFlushDelay`. + storage_->budgets_data()->DeleteData({}); + + std::move(done).Run(); +} + } // namespace content diff --git a/content/browser/private_aggregation/private_aggregation_budgeter.h b/content/browser/private_aggregation/private_aggregation_budgeter.h index 24a94cedbf07e4..a46db8396cd2b3 100644 --- a/content/browser/private_aggregation/private_aggregation_budgeter.h +++ b/content/browser/private_aggregation/private_aggregation_budgeter.h @@ -13,6 +13,7 @@ #include "base/time/time.h" #include "content/browser/private_aggregation/private_aggregation_budget_key.h" #include "content/common/content_export.h" +#include "content/public/browser/storage_partition.h" template class scoped_refptr; @@ -44,8 +45,9 @@ class CONTENT_EXPORT PrivateAggregationBudgeter { // Maximum budget allowed to be claimed per-origin per-day per-API. static constexpr int kMaxBudgetPerScope = 65536; - // To avoid unbounded memory growth, limit the number of pending consume - // budget calls during initialization. + // To avoid unbounded memory growth, limit the number of pending calls during + // initialization. Data clearing calls can be posted even if it would exceed + // this limit. static constexpr int kMaxPendingCalls = 1000; // The total length of time that per-origin per-API budgets are enforced @@ -87,6 +89,17 @@ class CONTENT_EXPORT PrivateAggregationBudgeter { const PrivateAggregationBudgetKey& budget_key, base::OnceCallback on_done); + // Deletes all data in storage for any budgets that could have been set + // between `delete_begin` and `delete_end` time (inclusive). Note that the + // discrete time windows used may lead to more data being deleted than + // strictly necessary. Null times are treated as unbounded lower or upper + // range. If `!filter.is_null()`, budget keys with an origin that does *not* + // match the `filter` are retained (i.e. not cleared). + virtual void ClearData(base::Time delete_begin, + base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done); + // TODO(crbug.com/1328439): Clear stale data periodically and on startup. protected: @@ -104,13 +117,18 @@ class CONTENT_EXPORT PrivateAggregationBudgeter { void ConsumeBudgetImpl(int additional_budget, const PrivateAggregationBudgetKey& budget_key, base::OnceCallback on_done); + void ClearDataImpl(base::Time delete_begin, + base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done); void ProcessAllPendingCalls(); - // While the storage initializes, queues calls to ConsumeBudget() in the - // order the calls are received. Should be empty after storage is initialized. - // The size is limited to `kMaxPendingCalls`. - std::vector pending_consume_budget_calls_; + // While the storage initializes, queues calls (e.g. to `ConsumeBudget()`) in + // the order the calls are received. Should be empty after storage is + // initialized. The size is limited to `kMaxPendingCalls` except that + // `ClearData()` can store additional tasks beyond that limit. + std::vector pending_calls_; // `nullptr` until initialization is complete or if initialization failed. // Otherwise, owned by this class until destruction. Iff present, diff --git a/content/browser/private_aggregation/private_aggregation_budgeter_unittest.cc b/content/browser/private_aggregation/private_aggregation_budgeter_unittest.cc index 5f40b51f2c5929..685a1f247d55b0 100644 --- a/content/browser/private_aggregation/private_aggregation_budgeter_unittest.cc +++ b/content/browser/private_aggregation/private_aggregation_budgeter_unittest.cc @@ -23,12 +23,18 @@ #include "base/test/task_environment.h" #include "base/time/time.h" #include "content/browser/private_aggregation/private_aggregation_budget_storage.h" +#include "content/browser/storage_partition_impl.h" #include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/storage_key/storage_key.h" #include "url/gurl.h" #include "url/origin.h" namespace content { +namespace { + +const base::Time kExampleTime = base::Time::FromJavaTime(1652984901234); + class PrivateAggregationBudgeterUnderTest : public PrivateAggregationBudgeter { public: PrivateAggregationBudgeterUnderTest( @@ -547,7 +553,7 @@ TEST_F(PrivateAggregationBudgeterTest, } TEST_F(PrivateAggregationBudgeterTest, - MaxPendingCallsExceeded_AdditionalCallsRejected) { + MaxPendingCallsExceeded_AdditionalConsumeBudgetCallsRejected) { base::RunLoop run_loop; CreateBudgeter(/*exclusively_run_in_memory=*/false, /*on_done_initializing=*/run_loop.QuitClosure()); @@ -591,6 +597,52 @@ TEST_F(PrivateAggregationBudgeterTest, PrivateAggregationBudgeter::StorageStatus::kOpen); } +TEST_F(PrivateAggregationBudgeterTest, + MaxPendingCallsExceeded_AdditionalDataClearingCallsAllowed) { + base::RunLoop run_loop; + CreateBudgeter(/*exclusively_run_in_memory=*/false, + /*on_done_initializing=*/run_loop.QuitClosure()); + + PrivateAggregationBudgetKey example_key = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), + base::Time::FromJavaTime(1652984901234), + PrivateAggregationBudgetKey::Api::kFledge); + + int num_consume_queries_succeeded = 0; + + for (int i = 0; i < PrivateAggregationBudgeter::kMaxPendingCalls; ++i) { + // Queries should be processed in the order they are received. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key, + base::BindLambdaForTesting( + [&num_consume_queries_succeeded, i](bool succeeded) { + EXPECT_TRUE(succeeded); + EXPECT_EQ(num_consume_queries_succeeded++, i); + })); + } + + EXPECT_EQ(num_consume_queries_succeeded, 0); + EXPECT_EQ(GetStorageStatus(), + PrivateAggregationBudgeter::StorageStatus::kInitializing); + + // Despite the limit being reached, data clearing requests are allowed to + // cause the limit to be exceeded and are queued. + bool was_callback_run = false; + budgeter()->ClearData( + base::Time::Min(), base::Time::Max(), + StoragePartition::StorageKeyMatcherFunction(), + base::BindLambdaForTesting([&]() { was_callback_run = true; })); + EXPECT_FALSE(was_callback_run); + + run_loop.Run(); + EXPECT_EQ(num_consume_queries_succeeded, + PrivateAggregationBudgeter::kMaxPendingCalls); + EXPECT_EQ(GetStorageStatus(), + PrivateAggregationBudgeter::StorageStatus::kOpen); + EXPECT_TRUE(was_callback_run); +} + TEST_F(PrivateAggregationBudgeterTest, BudgeterDestroyedImmediatelyAfterInitialization_DoesNotCrash) { base::RunLoop run_loop; @@ -611,4 +663,511 @@ TEST_F(PrivateAggregationBudgeterTest, base::RunLoop().RunUntilIdle(); } +TEST_F(PrivateAggregationBudgeterTest, ClearDataBasicTest) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + PrivateAggregationBudgetKey example_key = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), kExampleTime, + PrivateAggregationBudgetKey::Api::kFledge); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + })); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + })); + + budgeter()->ClearData( + kExampleTime, kExampleTime, StoragePartition::StorageKeyMatcherFunction(), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + base::RunLoop run_loop; + + // After clearing, we can use the full budget again + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key, + base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 4); +} + +TEST_F(PrivateAggregationBudgeterTest, ClearDataCrossesWindowBoundary) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + PrivateAggregationBudgetKey example_key_1 = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), kExampleTime, + PrivateAggregationBudgetKey::Api::kFledge); + + PrivateAggregationBudgetKey example_key_2 = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), + kExampleTime + PrivateAggregationBudgetKey::TimeWindow::kDuration, + PrivateAggregationBudgetKey::Api::kFledge); + + EXPECT_NE(example_key_1.time_window().start_time(), + example_key_2.time_window().start_time()); + + budgeter()->ConsumeBudget( + /*budget=*/1, example_key_1, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + })); + + budgeter()->ConsumeBudget( + /*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 1), + example_key_2, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + })); + + // The full budget has been used across the two time windows. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key_2, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + })); + + budgeter()->ClearData( + kExampleTime, + kExampleTime + PrivateAggregationBudgetKey::TimeWindow::kDuration, + StoragePartition::StorageKeyMatcherFunction(), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + base::RunLoop run_loop; + + // After clearing, we can use the full budget again. + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_2, + base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 5); +} + +TEST_F(PrivateAggregationBudgeterTest, + ClearDataDoesntAffectWindowsOutsideRange) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + PrivateAggregationBudgetKey key_to_clear = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), kExampleTime, + PrivateAggregationBudgetKey::Api::kFledge); + + PrivateAggregationBudgetKey key_after = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), + kExampleTime + PrivateAggregationBudgetKey::TimeWindow::kDuration, + PrivateAggregationBudgetKey::Api::kFledge); + + PrivateAggregationBudgetKey key_before = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), + kExampleTime - PrivateAggregationBudgetKey::TimeWindow::kDuration, + PrivateAggregationBudgetKey::Api::kFledge); + + EXPECT_LT(key_to_clear.time_window().start_time(), + key_after.time_window().start_time()); + + EXPECT_GT(key_to_clear.time_window().start_time(), + key_before.time_window().start_time()); + + base::RepeatingCallback expect_succeeded = + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + }); + + budgeter()->ConsumeBudget( + /*budget=*/1, key_before, expect_succeeded); + + budgeter()->ConsumeBudget( + /*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 2), + key_to_clear, expect_succeeded); + + budgeter()->ConsumeBudget( + /*budget=*/1, key_after, expect_succeeded); + + // The full budget has been used across the three time windows. + budgeter()->ConsumeBudget( + /*budget=*/1, key_after, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + })); + + // This will only clear the `key_to_clear`'s budget. + budgeter()->ClearData( + kExampleTime, kExampleTime, StoragePartition::StorageKeyMatcherFunction(), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + // After clearing, we can have a budget of exactly + // (`PrivateAggregationBudgeter::kMaxBudgetPerScope` - 2) that we can use. + budgeter()->ConsumeBudget( + /*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 2), + key_after, expect_succeeded); + + base::RunLoop run_loop; + budgeter()->ConsumeBudget( + /*budget=*/1, key_after, base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 7); +} + +TEST_F(PrivateAggregationBudgeterTest, ClearDataAllApisAffected) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + PrivateAggregationBudgetKey fledge_key = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), kExampleTime, + PrivateAggregationBudgetKey::Api::kFledge); + + PrivateAggregationBudgetKey shared_storage_key = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), kExampleTime, + PrivateAggregationBudgetKey::Api::kSharedStorage); + + base::RepeatingCallback expect_succeeded = + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + }); + base::RepeatingCallback expect_not_succeeded = + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + }); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, fledge_key, + expect_succeeded); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, fledge_key, expect_not_succeeded); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, + shared_storage_key, expect_succeeded); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, shared_storage_key, expect_not_succeeded); + + budgeter()->ClearData( + kExampleTime, kExampleTime, StoragePartition::StorageKeyMatcherFunction(), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + // After clearing, we can use the full budget again + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, fledge_key, + expect_succeeded); + base::RunLoop run_loop; + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, + shared_storage_key, base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 7); +} + +TEST_F(PrivateAggregationBudgeterTest, ClearAllDataBasicTest) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + PrivateAggregationBudgetKey example_key = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), kExampleTime, + PrivateAggregationBudgetKey::Api::kFledge); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + })); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + })); + + budgeter()->ClearData( + base::Time::Min(), base::Time::Max(), + StoragePartition::StorageKeyMatcherFunction(), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + base::RunLoop run_loop; + + // After clearing, we can use the full budget again + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key, + base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 4); +} + +TEST_F(PrivateAggregationBudgeterTest, ClearAllDataNullTimes) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + PrivateAggregationBudgetKey example_key = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), kExampleTime, + PrivateAggregationBudgetKey::Api::kFledge); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + })); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + })); + + budgeter()->ClearData( + base::Time(), base::Time(), StoragePartition::StorageKeyMatcherFunction(), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + base::RunLoop run_loop; + + // After clearing, we can use the full budget again + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key, + base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 4); +} + +TEST_F(PrivateAggregationBudgeterTest, ClearAllDataNullStartNonNullEndTime) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + PrivateAggregationBudgetKey example_key = + PrivateAggregationBudgetKey::CreateForTesting( + url::Origin::Create(GURL("https://a.example/")), kExampleTime, + PrivateAggregationBudgetKey::Api::kFledge); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + })); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key, + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + })); + + budgeter()->ClearData( + base::Time(), base::Time::Max(), + StoragePartition::StorageKeyMatcherFunction(), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + base::RunLoop run_loop; + + // After clearing, we can use the full budget again + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key, + base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 4); +} + +TEST_F(PrivateAggregationBudgeterTest, ClearDataFilterSelectsOrigins) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + const url::Origin kOriginA = url::Origin::Create(GURL("https://a.example/")); + const url::Origin kOriginB = url::Origin::Create(GURL("https://b.example/")); + + PrivateAggregationBudgetKey example_key_a = + PrivateAggregationBudgetKey::CreateForTesting( + kOriginA, kExampleTime, PrivateAggregationBudgetKey::Api::kFledge); + + PrivateAggregationBudgetKey example_key_b = + PrivateAggregationBudgetKey::CreateForTesting( + kOriginB, kExampleTime, PrivateAggregationBudgetKey::Api::kFledge); + + base::RepeatingCallback expect_succeeded = + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + }); + base::RepeatingCallback expect_not_succeeded = + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + }); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_a, + expect_succeeded); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key_a, expect_not_succeeded); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_b, + expect_succeeded); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key_b, expect_not_succeeded); + + budgeter()->ClearData( + kExampleTime, kExampleTime, + base::BindLambdaForTesting([&](const blink::StorageKey& storage_key) { + return storage_key == blink::StorageKey(kOriginA); + }), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + // After clearing, we can use the full budget again for the cleared origin. + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_a, + expect_succeeded); + base::RunLoop run_loop; + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_b, + base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 7); +} + +TEST_F(PrivateAggregationBudgeterTest, ClearDataAllTimeFilterSelectsOrigins) { + int num_queries_processed = 0; + + CreateBudgeterAndWait(); + + const url::Origin kOriginA = url::Origin::Create(GURL("https://a.example/")); + const url::Origin kOriginB = url::Origin::Create(GURL("https://b.example/")); + + PrivateAggregationBudgetKey example_key_a = + PrivateAggregationBudgetKey::CreateForTesting( + kOriginA, kExampleTime, PrivateAggregationBudgetKey::Api::kFledge); + + PrivateAggregationBudgetKey example_key_b = + PrivateAggregationBudgetKey::CreateForTesting( + kOriginB, kExampleTime, PrivateAggregationBudgetKey::Api::kFledge); + + base::RepeatingCallback expect_succeeded = + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_TRUE(succeeded); + ++num_queries_processed; + }); + base::RepeatingCallback expect_not_succeeded = + base::BindLambdaForTesting([&num_queries_processed](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + }); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_a, + expect_succeeded); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key_a, expect_not_succeeded); + + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_b, + expect_succeeded); + + // Maximum budget has been used so this should fail. + budgeter()->ConsumeBudget( + /*budget=*/1, example_key_b, expect_not_succeeded); + + budgeter()->ClearData( + base::Time::Min(), base::Time::Max(), + base::BindLambdaForTesting([&](const blink::StorageKey& storage_key) { + return storage_key == blink::StorageKey(kOriginA); + }), + base::BindLambdaForTesting([&]() { ++num_queries_processed; })); + + // After clearing, we can use the full budget again for the cleared origin. + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_a, + expect_succeeded); + base::RunLoop run_loop; + budgeter()->ConsumeBudget( + /*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_b, + base::BindLambdaForTesting([&](bool succeeded) { + EXPECT_FALSE(succeeded); + ++num_queries_processed; + run_loop.Quit(); + })); + run_loop.Run(); + EXPECT_EQ(num_queries_processed, 7); +} + +} // namespace + } // namespace content diff --git a/content/browser/private_aggregation/private_aggregation_manager.h b/content/browser/private_aggregation/private_aggregation_manager.h index 8abe8f9c6577ed..a92989042924cd 100644 --- a/content/browser/private_aggregation/private_aggregation_manager.h +++ b/content/browser/private_aggregation/private_aggregation_manager.h @@ -5,10 +5,16 @@ #ifndef CONTENT_BROWSER_PRIVATE_AGGREGATION_PRIVATE_AGGREGATION_MANAGER_H_ #define CONTENT_BROWSER_PRIVATE_AGGREGATION_PRIVATE_AGGREGATION_MANAGER_H_ +#include "base/callback_forward.h" #include "content/browser/private_aggregation/private_aggregation_budget_key.h" #include "content/common/private_aggregation_host.mojom-forward.h" +#include "content/public/browser/storage_partition.h" #include "mojo/public/cpp/bindings/pending_receiver.h" +namespace base { +class Time; +} + namespace url { class Origin; } @@ -35,6 +41,18 @@ class PrivateAggregationManager { PrivateAggregationBudgetKey::Api api_for_budgeting, mojo::PendingReceiver pending_receiver) = 0; + + // Deletes all data in storage for any budgets that could have been set + // between `delete_begin` and `delete_end` time (inclusive). Note that the + // discrete time windows used in the budgeter may lead to more data being + // deleted than strictly necessary. Null times are treated as unbounded lower + // or upper range. If `!filter.is_null()`, budget keys with an origin that + // does *not* match the `filter` are retained (i.e. not cleared). + virtual void ClearBudgetData( + base::Time delete_begin, + base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done) = 0; }; } // namespace content diff --git a/content/browser/private_aggregation/private_aggregation_manager_impl.cc b/content/browser/private_aggregation/private_aggregation_manager_impl.cc index 5b34908be8ba2d..04324780f88718 100644 --- a/content/browser/private_aggregation/private_aggregation_manager_impl.cc +++ b/content/browser/private_aggregation/private_aggregation_manager_impl.cc @@ -10,11 +10,13 @@ #include #include "base/bind.h" +#include "base/callback.h" #include "base/check.h" #include "base/files/file_path.h" #include "base/numerics/checked_math.h" #include "base/task/lazy_thread_pool_task_runner.h" #include "base/task/task_traits.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" @@ -23,6 +25,7 @@ #include "content/browser/storage_partition_impl.h" #include "content/common/aggregatable_report.mojom.h" #include "content/common/private_aggregation_host.mojom.h" +#include "content/public/browser/storage_partition.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "url/origin.h" @@ -85,6 +88,15 @@ bool PrivateAggregationManagerImpl::BindNewReceiver( 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)); +} + void PrivateAggregationManagerImpl::OnReportRequestReceivedFromHost( AggregatableReportRequest report_request, PrivateAggregationBudgetKey budget_key) { diff --git a/content/browser/private_aggregation/private_aggregation_manager_impl.h b/content/browser/private_aggregation/private_aggregation_manager_impl.h index 127f9f1ed613e2..2c7ddc3094a00f 100644 --- a/content/browser/private_aggregation/private_aggregation_manager_impl.h +++ b/content/browser/private_aggregation/private_aggregation_manager_impl.h @@ -7,15 +7,18 @@ #include +#include "base/callback_forward.h" #include "base/memory/raw_ptr.h" #include "content/browser/private_aggregation/private_aggregation_budget_key.h" #include "content/browser/private_aggregation/private_aggregation_manager.h" #include "content/common/content_export.h" #include "content/common/private_aggregation_host.mojom.h" +#include "content/public/browser/storage_partition.h" #include "mojo/public/cpp/bindings/pending_receiver.h" namespace base { class FilePath; +class Time; } namespace url { @@ -52,6 +55,10 @@ class CONTENT_EXPORT PrivateAggregationManagerImpl PrivateAggregationBudgetKey::Api api_for_budgeting, mojo::PendingReceiver pending_receiver) override; + void ClearBudgetData(base::Time delete_begin, + base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done) override; protected: // Protected for testing. diff --git a/content/browser/private_aggregation/private_aggregation_manager_impl_unittest.cc b/content/browser/private_aggregation/private_aggregation_manager_impl_unittest.cc index 8808f8a051abac..0eba4dfdba46de 100644 --- a/content/browser/private_aggregation/private_aggregation_manager_impl_unittest.cc +++ b/content/browser/private_aggregation/private_aggregation_manager_impl_unittest.cc @@ -12,8 +12,10 @@ #include "base/callback_helpers.h" #include "base/files/file_path.h" #include "base/memory/ptr_util.h" +#include "base/run_loop.h" #include "base/task/sequenced_task_runner.h" #include "base/task/thread_pool.h" +#include "base/test/bind.h" #include "base/time/time.h" #include "content/browser/aggregation_service/aggregatable_report.h" #include "content/browser/aggregation_service/aggregation_service.h" @@ -28,6 +30,7 @@ #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/blink/public/common/storage_key/storage_key.h" #include "url/gurl.h" #include "url/origin.h" @@ -287,4 +290,46 @@ TEST_F(PrivateAggregationManagerImplTest, mojo::PendingReceiver())); } +TEST_F(PrivateAggregationManagerImplTest, + ClearBudgetingData_InvokesClearDataIdentically) { + { + base::RunLoop run_loop; + EXPECT_CALL(*budgeter_, + ClearData(kExampleTime, kExampleTime + base::Days(1), _, _)) + .WillOnce(Invoke([](base::Time delete_begin, base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done) { + EXPECT_TRUE(filter.is_null()); + std::move(done).Run(); + })); + manager_.ClearBudgetData(kExampleTime, kExampleTime + base::Days(1), + StoragePartition::StorageKeyMatcherFunction(), + run_loop.QuitClosure()); + run_loop.Run(); + } + + StoragePartition::StorageKeyMatcherFunction example_filter; + example_filter = + base::BindLambdaForTesting([](const blink::StorageKey& storage_key) { + return storage_key.origin() == + url::Origin::Create(GURL("https://example.com")); + }); + + { + base::RunLoop run_loop; + EXPECT_CALL(*budgeter_, + ClearData(kExampleTime - base::Days(10), kExampleTime, _, _)) + .WillOnce(Invoke([&example_filter]( + base::Time delete_begin, base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done) { + EXPECT_EQ(filter, example_filter); + std::move(done).Run(); + })); + manager_.ClearBudgetData(kExampleTime - base::Days(10), kExampleTime, + example_filter, run_loop.QuitClosure()); + run_loop.Run(); + } +} + } // namespace content diff --git a/content/browser/private_aggregation/private_aggregation_test_utils.cc b/content/browser/private_aggregation/private_aggregation_test_utils.cc index 9327e2cb5c24a4..669de520b824be 100644 --- a/content/browser/private_aggregation/private_aggregation_test_utils.cc +++ b/content/browser/private_aggregation/private_aggregation_test_utils.cc @@ -21,6 +21,9 @@ MockPrivateAggregationHost::MockPrivateAggregationHost() MockPrivateAggregationHost::~MockPrivateAggregationHost() = default; +MockPrivateAggregationManager::MockPrivateAggregationManager() = default; +MockPrivateAggregationManager::~MockPrivateAggregationManager() = default; + MockPrivateAggregationContentBrowserClient:: MockPrivateAggregationContentBrowserClient() = default; diff --git a/content/browser/private_aggregation/private_aggregation_test_utils.h b/content/browser/private_aggregation/private_aggregation_test_utils.h index ac4696f712253d..c0474793aaf278 100644 --- a/content/browser/private_aggregation/private_aggregation_test_utils.h +++ b/content/browser/private_aggregation/private_aggregation_test_utils.h @@ -12,12 +12,18 @@ #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_host.h" +#include "content/browser/private_aggregation/private_aggregation_manager.h" #include "content/common/aggregatable_report.mojom-forward.h" +#include "content/public/browser/storage_partition.h" #include "content/public/test/test_browser_context.h" #include "content/test/test_content_browser_client.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "testing/gmock/include/gmock/gmock.h" +namespace base { +class Time; +} + namespace url { class Origin; } @@ -35,6 +41,14 @@ class MockPrivateAggregationBudgeter : public PrivateAggregationBudgeter { const PrivateAggregationBudgetKey&, base::OnceCallback), (override)); + + MOCK_METHOD(void, + ClearData, + (base::Time, + base::Time, + StoragePartition::StorageKeyMatcherFunction, + base::OnceClosure), + (override)); }; // Note: the `TestBrowserContext` may require a `BrowserTaskEnvironment` to be @@ -55,13 +69,35 @@ class MockPrivateAggregationHost : public PrivateAggregationHost { MOCK_METHOD(void, SendHistogramReport, (std::vector, - mojom::AggregationServiceMode aggregation_mode), + mojom::AggregationServiceMode), (override)); private: TestBrowserContext test_browser_context_; }; +class MockPrivateAggregationManager : public PrivateAggregationManager { + public: + MockPrivateAggregationManager(); + ~MockPrivateAggregationManager() override; + + MOCK_METHOD(bool, + BindNewReceiver, + (url::Origin, + url::Origin, + PrivateAggregationBudgetKey::Api, + mojo::PendingReceiver), + (override)); + + MOCK_METHOD(void, + ClearBudgetData, + (base::Time, + base::Time, + StoragePartition::StorageKeyMatcherFunction, + base::OnceClosure), + (override)); +}; + class MockPrivateAggregationContentBrowserClient : public TestContentBrowserClient { public: diff --git a/content/browser/storage_partition_impl.cc b/content/browser/storage_partition_impl.cc index ed2ca3323d9bc7..e3d2e62326f206 100644 --- a/content/browser/storage_partition_impl.cc +++ b/content/browser/storage_partition_impl.cc @@ -79,6 +79,7 @@ #include "content/browser/notifications/platform_notification_context_impl.h" #include "content/browser/payments/payment_app_context_impl.h" #include "content/browser/preloading/prerender/prerender_host_registry.h" +#include "content/browser/private_aggregation/private_aggregation_manager.h" #include "content/browser/private_aggregation/private_aggregation_manager_impl.h" #include "content/browser/push_messaging/push_messaging_context.h" #include "content/browser/quota/quota_context.h" @@ -972,6 +973,7 @@ class StoragePartitionImpl::DataDeletionHelper { InterestGroupManagerImpl* interest_group_manager, AttributionManager* attribution_manager, AggregationService* aggregation_service, + PrivateAggregationManager* private_aggregation_manager, storage::SharedStorageManager* shared_storage_manager, bool perform_storage_cleanup, const base::Time begin, @@ -1004,7 +1006,8 @@ class StoragePartitionImpl::DataDeletionHelper { kAggregationService = 9, kSharedStorage = 10, kGpuCache = 11, - kMaxValue = kGpuCache, + kPrivateAggregation = 12, + kMaxValue = kPrivateAggregation, }; base::OnceClosure CreateTaskCompletionClosure(TracingDataType data_type); @@ -1725,7 +1728,7 @@ storage::SharedStorageManager* StoragePartitionImpl::GetSharedStorageManager() { return shared_storage_manager_.get(); } -PrivateAggregationManagerImpl* +PrivateAggregationManager* StoragePartitionImpl::GetPrivateAggregationManager() { DCHECK(initialized_); return private_aggregation_manager_.get(); @@ -2202,8 +2205,8 @@ void StoragePartitionImpl::ClearDataImpl( quota_manager_.get(), special_storage_policy_.get(), filesystem_context_.get(), GetCookieManagerForBrowserProcess(), interest_group_manager_.get(), attribution_manager_.get(), - aggregation_service_.get(), shared_storage_manager_.get(), - perform_storage_cleanup, begin, end); + aggregation_service_.get(), private_aggregation_manager_.get(), + shared_storage_manager_.get(), perform_storage_cleanup, begin, end); } void StoragePartitionImpl::DeletionHelperDone(base::OnceClosure callback) { @@ -2405,6 +2408,7 @@ void StoragePartitionImpl::DataDeletionHelper::ClearDataOnUIThread( InterestGroupManagerImpl* interest_group_manager, AttributionManager* attribution_manager, AggregationService* aggregation_service, + PrivateAggregationManager* private_aggregation_manager, storage::SharedStorageManager* shared_storage_manager, bool perform_storage_cleanup, const base::Time begin, @@ -2547,6 +2551,13 @@ void StoragePartitionImpl::DataDeletionHelper::ClearDataOnUIThread( CreateTaskCompletionClosure(TracingDataType::kAggregationService)); } + if (private_aggregation_manager && + (remove_mask_ & REMOVE_DATA_MASK_PRIVATE_AGGREGATION_INTERNAL)) { + private_aggregation_manager->ClearBudgetData( + begin, end, filter, + CreateTaskCompletionClosure(TracingDataType::kPrivateAggregation)); + } + // TODO(crbug.com/1340250): The Plugin Private File System is removed, but // some devices may still have old data on their machine. For now greedily try // to delete this data, but we'll want to remove this code at some point. @@ -2830,8 +2841,7 @@ void StoragePartitionImpl::OverrideAttributionManagerForTesting( } void StoragePartitionImpl::OverridePrivateAggregationManagerForTesting( - std::unique_ptr - private_aggregation_manager) { + std::unique_ptr private_aggregation_manager) { DCHECK(initialized_); private_aggregation_manager_ = std::move(private_aggregation_manager); } diff --git a/content/browser/storage_partition_impl.h b/content/browser/storage_partition_impl.h index 7839dfd1643f5e..4555678ad0fb9b 100644 --- a/content/browser/storage_partition_impl.h +++ b/content/browser/storage_partition_impl.h @@ -88,7 +88,7 @@ class MediaLicenseManager; class NativeIOContextImpl; class PaymentAppContextImpl; class PrefetchURLLoaderService; -class PrivateAggregationManagerImpl; +class PrivateAggregationManager; class PushMessagingContext; class QuotaContext; class SharedStorageWorkletHostManager; @@ -145,8 +145,7 @@ class CONTENT_EXPORT StoragePartitionImpl void OverrideAttributionManagerForTesting( std::unique_ptr attribution_manager); void OverridePrivateAggregationManagerForTesting( - std::unique_ptr - private_aggregation_manager); + std::unique_ptr private_aggregation_manager); // Returns the StoragePartitionConfig that represents this StoragePartition. const StoragePartitionConfig& GetConfig(); @@ -265,7 +264,7 @@ class CONTENT_EXPORT StoragePartitionImpl // Gets the SharedStorageManager for the StoragePartition, or nullptr if it // doesn't exist because the feature is disabled. storage::SharedStorageManager* GetSharedStorageManager(); - PrivateAggregationManagerImpl* GetPrivateAggregationManager(); + PrivateAggregationManager* GetPrivateAggregationManager(); // blink::mojom::DomStorage interface. void OpenLocalStorage( @@ -670,7 +669,7 @@ class CONTENT_EXPORT StoragePartitionImpl std::unique_ptr shared_storage_worklet_host_manager_; - std::unique_ptr private_aggregation_manager_; + std::unique_ptr private_aggregation_manager_; // ReceiverSet for DomStorage, using the // ChildProcessSecurityPolicyImpl::Handle as the binding context type. The diff --git a/content/browser/storage_partition_impl_unittest.cc b/content/browser/storage_partition_impl_unittest.cc index c9d893b92e6cca..e641937b470cbf 100644 --- a/content/browser/storage_partition_impl_unittest.cc +++ b/content/browser/storage_partition_impl_unittest.cc @@ -58,6 +58,8 @@ #include "content/browser/interest_group/interest_group_manager_impl.h" #include "content/browser/interest_group/interest_group_permissions_cache.h" #include "content/browser/interest_group/interest_group_permissions_checker.h" +#include "content/browser/private_aggregation/private_aggregation_manager.h" +#include "content/browser/private_aggregation/private_aggregation_test_utils.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/generated_code_cache_settings.h" @@ -2150,6 +2152,115 @@ TEST_F(StoragePartitionImplTest, RemoveAggregationServiceData) { } } +TEST_F(StoragePartitionImplTest, RemovePrivateAggregationData) { + StoragePartitionImpl* partition = static_cast( + browser_context()->GetDefaultStoragePartition()); + + auto private_aggregation_manager = + std::make_unique(); + auto* private_aggregation_manager_ptr = private_aggregation_manager.get(); + partition->OverridePrivateAggregationManagerForTesting( + std::move(private_aggregation_manager)); + + const uint32_t kTestClearMask = + StoragePartition::REMOVE_DATA_MASK_PRIVATE_AGGREGATION_INTERNAL; + const uint32_t kTestQuotaClearMask = + StoragePartition::QUOTA_MANAGED_STORAGE_MASK_ALL; + const auto kTestOrigin = GURL("https://example.com"); + const auto kOtherOrigin = GURL("https://example.net"); + const auto kBeginTime = base::Time() + base::Hours(1); + const auto kEndTime = base::Time() + base::Hours(2); + const auto invoke_callback = + [](base::Time delete_begin, base::Time delete_end, + StoragePartition::StorageKeyMatcherFunction filter, + base::OnceClosure done) { std::move(done).Run(); }; + const auto is_test_origin_valid = + [&kTestOrigin]( + content::StoragePartition::StorageKeyMatcherFunction filter) { + return filter.Run(blink::StorageKey(url::Origin::Create(kTestOrigin))); + }; + const auto is_other_origin_valid = + [&kOtherOrigin]( + content::StoragePartition::StorageKeyMatcherFunction filter) { + return filter.Run(blink::StorageKey(url::Origin::Create(kOtherOrigin))); + }; + const auto is_filter_null = + [&](content::StoragePartition::StorageKeyMatcherFunction filter) { + return filter.is_null(); + }; + + // Verify that each of the StoragePartition interfaces for clearing origin + // based data calls aggregation service appropriately. + EXPECT_CALL( + *private_aggregation_manager_ptr, + ClearBudgetData( + base::Time(), base::Time::Max(), + testing::AllOf(testing::Truly(is_test_origin_valid), + testing::Not(testing::Truly(is_other_origin_valid))), + testing::_)) + .WillOnce(invoke_callback); + { + base::RunLoop run_loop; + partition->ClearDataForOrigin(kTestClearMask, kTestQuotaClearMask, + kTestOrigin, run_loop.QuitClosure()); + run_loop.Run(); + testing::Mock::VerifyAndClearExpectations(private_aggregation_manager_ptr); + } + + EXPECT_CALL( + *private_aggregation_manager_ptr, + ClearBudgetData( + kBeginTime, kEndTime, + testing::AllOf(testing::Truly(is_test_origin_valid), + testing::Not(testing::Truly(is_other_origin_valid))), + testing::_)) + .WillOnce(testing::Invoke(invoke_callback)); + { + base::RunLoop run_loop; + partition->ClearData(kTestClearMask, kTestQuotaClearMask, + blink::StorageKey(url::Origin::Create(kTestOrigin)), + kBeginTime, kEndTime, run_loop.QuitClosure()); + run_loop.Run(); + testing::Mock::VerifyAndClearExpectations(private_aggregation_manager_ptr); + } + + EXPECT_CALL( + *private_aggregation_manager_ptr, + ClearBudgetData( + kBeginTime, kEndTime, + testing::AllOf(testing::Truly(is_test_origin_valid), + testing::Not(testing::Truly(is_other_origin_valid))), + testing::_)) + .WillOnce(testing::Invoke(invoke_callback)); + { + base::RunLoop run_loop; + partition->ClearData( + kTestClearMask, kTestQuotaClearMask, + base::BindLambdaForTesting([&](const blink::StorageKey& storage_key, + storage::SpecialStoragePolicy* policy) { + return storage_key == + blink::StorageKey(url::Origin::Create(kTestOrigin)); + }), + /*cookie_deletion_filter=*/nullptr, + /*perform_storage_cleanup=*/false, kBeginTime, kEndTime, + run_loop.QuitClosure()); + run_loop.Run(); + testing::Mock::VerifyAndClearExpectations(private_aggregation_manager_ptr); + } + + EXPECT_CALL(*private_aggregation_manager_ptr, + ClearBudgetData(kBeginTime, kEndTime, + testing::Truly(is_filter_null), testing::_)) + .WillOnce(testing::Invoke(invoke_callback)); + { + base::RunLoop run_loop; + partition->ClearData(kTestClearMask, kTestQuotaClearMask, + blink::StorageKey(), kBeginTime, kEndTime, + run_loop.QuitClosure()); + run_loop.Run(); + } +} + // https://crbug.com/1221382 // Make sure StorageServiceImpl can be stored in a SequenceLocalStorageSlot and // that it can be safely destroyed when the thread terminates. diff --git a/content/public/browser/browsing_data_remover.h b/content/public/browser/browsing_data_remover.h index ba28b4bafa4ffb..5e83a3db84ca9e 100644 --- a/content/public/browser/browsing_data_remover.h +++ b/content/public/browser/browsing_data_remover.h @@ -131,9 +131,16 @@ class BrowsingDataRemover { // information. static constexpr DataType DATA_TYPE_ATTRIBUTION_REPORTING_INTERNAL = 1 << 21; + // Private Aggregation API + // (https://github.com/alexmturner/private-aggregation-api) persistent + // storage. This only refers to data stored internally by the API, such as + // privacy budgeting information. Note that currently the API does not persist + // any other data. Should only be cleared by user-initiated deletions. + static constexpr DataType DATA_TYPE_PRIVATE_AGGREGATION_INTERNAL = 1 << 22; + // Embedders can add more datatypes beyond this point. static constexpr DataType DATA_TYPE_CONTENT_END = - DATA_TYPE_ATTRIBUTION_REPORTING_INTERNAL; + DATA_TYPE_PRIVATE_AGGREGATION_INTERNAL; // All data stored by the Attribution Reporting API. static constexpr DataType DATA_TYPE_ATTRIBUTION_REPORTING = @@ -144,7 +151,13 @@ class BrowsingDataRemover { static constexpr DataType DATA_TYPE_PRIVACY_SANDBOX = DATA_TYPE_TRUST_TOKENS | DATA_TYPE_ATTRIBUTION_REPORTING | DATA_TYPE_AGGREGATION_SERVICE | DATA_TYPE_INTEREST_GROUPS | - DATA_TYPE_SHARED_STORAGE; + DATA_TYPE_SHARED_STORAGE | DATA_TYPE_PRIVATE_AGGREGATION_INTERNAL; + + // Internal data stored by APIs in the Privacy Sandbox, e.g. privacy budgeting + // information. + static constexpr DataType DATA_TYPE_PRIVACY_SANDBOX_INTERNAL = + DATA_TYPE_ATTRIBUTION_REPORTING_INTERNAL | + DATA_TYPE_PRIVATE_AGGREGATION_INTERNAL; using OriginType = uint64_t; // Web storage origins that StoragePartition recognizes as NOT protected diff --git a/content/public/browser/storage_partition.h b/content/public/browser/storage_partition.h index b89192609b6aa4..6d40d3c3880e40 100644 --- a/content/public/browser/storage_partition.h +++ b/content/public/browser/storage_partition.h @@ -189,6 +189,7 @@ class CONTENT_EXPORT StoragePartition { REMOVE_DATA_MASK_INTEREST_GROUP_PERMISSIONS_CACHE = 1 << 15, REMOVE_DATA_MASK_ATTRIBUTION_REPORTING_INTERNAL = 1 << 16, + REMOVE_DATA_MASK_PRIVATE_AGGREGATION_INTERNAL = 1 << 17, REMOVE_DATA_MASK_ALL = 0xFFFFFFFF, diff --git a/weblayer/browser/profile_impl.cc b/weblayer/browser/profile_impl.cc index 8748a5f3f3cd57..aa6e5cccdaf6b1 100644 --- a/weblayer/browser/profile_impl.cc +++ b/weblayer/browser/profile_impl.cc @@ -304,6 +304,8 @@ void ProfileImpl::ClearBrowsingData( content::BrowsingDataRemover::DATA_TYPE_ATTRIBUTION_REPORTING; remove_mask |= content::BrowsingDataRemover::DATA_TYPE_AGGREGATION_SERVICE; + remove_mask |= content::BrowsingDataRemover:: + DATA_TYPE_PRIVATE_AGGREGATION_INTERNAL; break; case BrowsingDataType::CACHE: remove_mask |= content::BrowsingDataRemover::DATA_TYPE_CACHE;