blob: 6febabb6da286a22b76ab9767767c53bc269da74 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/attribution_reporting/rate_limit_table.h"
#include "base/check.h"
#include "base/time/clock.h"
#include "content/browser/attribution_reporting/attribution_report.h"
#include "content/browser/attribution_reporting/sql_utils.h"
#include "net/base/schemeful_site.h"
#include "sql/database.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "url/origin.h"
namespace content {
namespace {
using AttributionAllowedStatus =
::content::RateLimitTable::AttributionAllowedStatus;
using AttributionType = ::content::AttributionStorage::AttributionType;
constexpr AttributionType kAttributionTypes[] = {
AttributionType::kNavigation,
AttributionType::kEvent,
AttributionType::kAggregate,
};
WARN_UNUSED_RESULT AttributionType
AttributionTypeFromSourceType(StorableSource::SourceType source_type) {
switch (source_type) {
case StorableSource::SourceType::kNavigation:
return AttributionType::kNavigation;
case StorableSource::SourceType::kEvent:
return AttributionType::kEvent;
}
}
WARN_UNUSED_RESULT int SerializeAttributionType(
AttributionType attribution_type) {
return static_cast<int>(attribution_type);
}
} // namespace
RateLimitTable::RateLimitTable(const AttributionStorage::Delegate* delegate,
const base::Clock* clock)
: delegate_(delegate), clock_(clock) {
DCHECK(delegate_);
DETACH_FROM_SEQUENCE(sequence_checker_);
}
RateLimitTable::~RateLimitTable() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
bool RateLimitTable::CreateTable(sql::Database* db) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// All columns in this table are const.
// |attribution_type| corresponds to the `AttributionType` enum.
// |impression_id| is the primary key of a row in the |impressions| table,
// though the row may not exist.
// |impression_site| is the eTLD+1 of the impression.
// |impression_origin| is the origin of the impression.
// |conversion_destination| is the destination of the conversion.
// |conversion_origin| is the origin of the conversion.
// |conversion_time| is the report's conversion time.
// |bucket| is the bucket for aggregate histogram contributions. It is unused
// for now, but we want the table to be extensible to a number of different
// types of limits. These include limiting total contributions across all
// buckets, as well as limiting the number of contributions to any one bucket.
// |value| is the magnitude of this contribution when compared against rate
// limits. For event-level rows, this is 1. For aggregate contributions, this
// is the value of the individual contribution to a bucket.
static constexpr char kRateLimitTableSql[] =
"CREATE TABLE IF NOT EXISTS rate_limits"
"(rate_limit_id INTEGER PRIMARY KEY NOT NULL,"
"attribution_type INTEGER NOT NULL,"
"impression_id INTEGER NOT NULL,"
"impression_site TEXT NOT NULL,"
"impression_origin TEXT NOT NULL,"
"conversion_destination TEXT NOT NULL,"
"conversion_origin TEXT NOT NULL,"
"conversion_time INTEGER NOT NULL,"
"bucket TEXT NOT NULL,"
"value INTEGER NOT NULL)";
if (!db->Execute(kRateLimitTableSql))
return false;
// Optimizes calls to |AttributionAllowed()|.
static constexpr char kRateLimitImpressionSiteTypeIndexSql[] =
"CREATE INDEX IF NOT EXISTS rate_limit_impression_site_type_idx "
"ON rate_limits(attribution_type,conversion_destination,"
"impression_site,conversion_time)";
if (!db->Execute(kRateLimitImpressionSiteTypeIndexSql))
return false;
// Optimizes calls to |DeleteExpiredRateLimits()|, |ClearAllDataInRange()|,
// |ClearDataForOriginsInRange()|.
static constexpr char kRateLimitAttributionTypeConversionTimeIndexSql[] =
"CREATE INDEX IF NOT EXISTS "
"rate_limit_attribution_type_conversion_time_idx "
"ON rate_limits(attribution_type,conversion_time)";
if (!db->Execute(kRateLimitAttributionTypeConversionTimeIndexSql))
return false;
// Optimizes calls to |ClearDataForSourceIds()|.
static constexpr char kRateLimitImpressionIndexSql[] =
"CREATE INDEX IF NOT EXISTS rate_limit_impression_id_idx "
"ON rate_limits(impression_id)";
return db->Execute(kRateLimitImpressionIndexSql);
}
bool RateLimitTable::AddRateLimit(sql::Database* db,
const AttributionReport& report) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(report.impression.impression_id().has_value());
return AddRow(db,
AttributionTypeFromSourceType(report.impression.source_type()),
*report.impression.impression_id(),
report.impression.ImpressionSite().Serialize(),
SerializeOrigin(report.impression.impression_origin()),
report.impression.ConversionDestination().Serialize(),
SerializeOrigin(report.impression.conversion_origin()),
report.conversion_time,
// Rate limits for the event-level API do not have a bucket.
/*bucket=*/"",
// By supplying 1 here, rate limits for the event-level API act
// as a count.
/*value=*/1u);
}
bool RateLimitTable::AddRow(
sql::Database* db,
AttributionType attribution_type,
StorableSource::Id source_id,
const std::string& serialized_impression_site,
const std::string& serialized_impression_origin,
const std::string& serialized_conversion_destination,
const std::string& serialized_conversion_origin,
base::Time time,
const std::string& bucket,
uint32_t value) {
// Only delete expired rate limits periodically to avoid excessive DB
// operations.
const base::TimeDelta delete_frequency =
delegate_->GetDeleteExpiredRateLimitsFrequency();
DCHECK_GE(delete_frequency, base::TimeDelta());
const base::Time now = clock_->Now();
if (now - last_cleared_ >= delete_frequency) {
if (!DeleteExpiredRateLimits(db, attribution_type))
return false;
last_cleared_ = now;
}
static constexpr char kStoreRateLimitSql[] =
"INSERT INTO rate_limits"
"(attribution_type,impression_id,impression_site,impression_origin,"
"conversion_destination,conversion_origin,conversion_time,bucket,value)"
"VALUES(?,?,?,?,?,?,?,?,?)";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kStoreRateLimitSql));
statement.BindInt(0, SerializeAttributionType(attribution_type));
statement.BindInt64(1, *source_id);
statement.BindString(2, serialized_impression_site);
statement.BindString(3, serialized_impression_origin);
statement.BindString(4, serialized_conversion_destination);
statement.BindString(5, serialized_conversion_origin);
statement.BindTime(6, time);
statement.BindString(7, bucket);
statement.BindInt64(8, static_cast<int64_t>(value));
return statement.Run();
}
AttributionAllowedStatus RateLimitTable::AttributionAllowed(
sql::Database* db,
const AttributionReport& report,
base::Time now) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const std::string serialized_impression_site =
report.impression.ImpressionSite().Serialize();
const std::string serialized_conversion_destination =
report.impression.ConversionDestination().Serialize();
const int64_t capacity = GetCapacity(
db, AttributionTypeFromSourceType(report.impression.source_type()),
serialized_impression_site, serialized_conversion_destination, now);
// This should only be possible if there is DB corruption.
if (capacity < 0)
return AttributionAllowedStatus::kError;
if (capacity == 0)
return AttributionAllowedStatus::kNotAllowed;
return AttributionAllowedStatus::kAllowed;
}
int64_t RateLimitTable::GetCapacity(
sql::Database* db,
AttributionType attribution_type,
const std::string& serialized_impression_site,
const std::string& serialized_conversion_destination,
base::Time now) {
const AttributionStorage::Delegate::RateLimitConfig rate_limits =
delegate_->GetRateLimits(attribution_type);
DCHECK_GT(rate_limits.time_window, base::TimeDelta());
DCHECK_GT(rate_limits.max_contributions_per_window, 0);
base::Time min_timestamp = now - rate_limits.time_window;
static constexpr char kAttributionAllowedSql[] =
"SELECT value FROM rate_limits "
DCHECK_SQL_INDEXED_BY("rate_limit_impression_site_type_idx")
"WHERE attribution_type = ? "
"AND impression_site = ? "
"AND conversion_destination = ? "
"AND conversion_time > ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kAttributionAllowedSql));
statement.BindInt(0, SerializeAttributionType(attribution_type));
statement.BindString(1, serialized_impression_site);
statement.BindString(2, serialized_conversion_destination);
statement.BindTime(3, min_timestamp);
int64_t sum = 0;
while (statement.Step()) {
int64_t value = statement.ColumnInt64(0);
sum += value;
}
if (!statement.Succeeded())
return -1;
return rate_limits.max_contributions_per_window > sum
? rate_limits.max_contributions_per_window - sum
: 0;
}
bool RateLimitTable::ClearAllDataInRange(sql::Database* db,
base::Time delete_begin,
base::Time delete_end) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!((delete_begin.is_null() || delete_begin.is_min()) &&
delete_end.is_max()));
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
static constexpr char kDeleteRateLimitRangeSql[] =
"DELETE FROM rate_limits "
DCHECK_SQL_INDEXED_BY("rate_limit_attribution_type_conversion_time_idx")
"WHERE attribution_type = ? AND conversion_time BETWEEN ? AND ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteRateLimitRangeSql));
for (AttributionType attribution_type : kAttributionTypes) {
statement.Reset(/*clear_bound_vars=*/true);
statement.BindInt(0, SerializeAttributionType(attribution_type));
statement.BindTime(1, delete_begin);
statement.BindTime(2, delete_end);
if (!statement.Run())
return false;
}
return transaction.Commit();
}
bool RateLimitTable::ClearAllDataAllTime(sql::Database* db) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
static constexpr char kDeleteAllRateLimitsSql[] = "DELETE FROM rate_limits";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteAllRateLimitsSql));
return statement.Run();
}
bool RateLimitTable::ClearDataForOriginsInRange(
sql::Database* db,
base::Time delete_begin,
base::Time delete_end,
base::RepeatingCallback<bool(const url::Origin&)> filter) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!filter.is_null());
std::vector<int64_t> rate_limit_ids_to_delete;
{
static constexpr char kScanCandidateData[] =
"SELECT rate_limit_id,impression_site,impression_origin,"
"conversion_destination,conversion_origin FROM rate_limits "
DCHECK_SQL_INDEXED_BY("rate_limit_attribution_type_conversion_time_idx")
"WHERE attribution_type = ? AND conversion_time BETWEEN ? AND ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kScanCandidateData));
// Issue deletes for different attribution_types so this can be easily
// optimized by the rate_limit_attribution_type_conversion_time_idx.
for (AttributionType attribution_type : kAttributionTypes) {
statement.Reset(/*clear_bound_vars=*/true);
statement.BindInt(0, SerializeAttributionType(attribution_type));
statement.BindTime(1, delete_begin);
statement.BindTime(2, delete_end);
while (statement.Step()) {
int64_t rate_limit_id = statement.ColumnInt64(0);
if (filter.Run(DeserializeOrigin(statement.ColumnString(1))) ||
filter.Run(DeserializeOrigin(statement.ColumnString(2))) ||
filter.Run(DeserializeOrigin(statement.ColumnString(3))) ||
filter.Run(DeserializeOrigin(statement.ColumnString(4)))) {
rate_limit_ids_to_delete.push_back(rate_limit_id);
}
}
if (!statement.Succeeded())
return false;
}
}
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
static constexpr char kDeleteRateLimitSql[] =
"DELETE FROM rate_limits WHERE rate_limit_id = ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteRateLimitSql));
for (int64_t rate_limit_id : rate_limit_ids_to_delete) {
statement.Reset(/*clear_bound_vars=*/true);
statement.BindInt64(0, rate_limit_id);
if (!statement.Run())
return false;
}
return transaction.Commit();
}
bool RateLimitTable::DeleteExpiredRateLimits(sql::Database* db,
AttributionType attribution_type) {
base::Time timestamp =
clock_->Now() - delegate_->GetRateLimits(attribution_type).time_window;
static constexpr char kDeleteExpiredRateLimits[] =
// clang-format off
"DELETE FROM rate_limits "
DCHECK_SQL_INDEXED_BY("rate_limit_attribution_type_conversion_time_idx")
"WHERE attribution_type = ? AND conversion_time <= ?"; // clang-format on
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteExpiredRateLimits));
statement.BindInt(0, SerializeAttributionType(attribution_type));
statement.BindTime(1, timestamp);
return statement.Run();
}
bool RateLimitTable::ClearDataForSourceIds(
sql::Database* db,
const std::vector<StorableSource::Id>& source_ids) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
static constexpr char kDeleteRateLimitSql[] =
"DELETE FROM rate_limits WHERE impression_id = ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kDeleteRateLimitSql));
for (StorableSource::Id id : source_ids) {
statement.Reset(/*clear_bound_vars=*/true);
statement.BindInt64(0, *id);
if (!statement.Run())
return false;
}
return transaction.Commit();
}
AttributionAllowedStatus
RateLimitTable::AddAggregateHistogramContributionsForTesting(
sql::Database* db,
const StorableSource& source,
const std::vector<AggregateHistogramContribution>& contributions) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(source.impression_id().has_value());
base::Time now = clock_->Now();
const std::string serialized_impression_site =
source.ImpressionSite().Serialize();
const std::string serialized_conversion_destination =
source.ConversionDestination().Serialize();
const int64_t capacity =
GetCapacity(db, AttributionType::kAggregate, serialized_impression_site,
serialized_conversion_destination, now);
// This should only be possible if there is DB corruption.
if (capacity < 0)
return AttributionAllowedStatus::kError;
if (capacity == 0)
return AttributionAllowedStatus::kNotAllowed;
int64_t new_sum = 0;
for (const auto& contribution : contributions) {
DCHECK_GT(contribution.value, 0u);
new_sum += contribution.value;
}
if (new_sum > capacity)
return AttributionAllowedStatus::kNotAllowed;
sql::Transaction transaction(db);
if (!transaction.Begin())
return AttributionAllowedStatus::kError;
const std::string serialized_impression_origin =
SerializeOrigin(source.impression_origin());
const std::string serialized_conversion_origin =
SerializeOrigin(source.conversion_origin());
for (const auto& contribution : contributions) {
if (!AddRow(db, AttributionType::kAggregate, *source.impression_id(),
serialized_impression_site, serialized_impression_origin,
serialized_conversion_destination, serialized_conversion_origin,
now, contribution.bucket, contribution.value)) {
return AttributionAllowedStatus::kError;
}
}
return transaction.Commit() ? AttributionAllowedStatus::kAllowed
: AttributionAllowedStatus::kError;
}
} // namespace content