blob: 7f240a09f965ae2abc88e8a5448bc895b6b5324a [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/interest_group/interest_group_permissions_checker.h"
#include "base/functional/callback.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "content/browser/interest_group/interest_group_permissions_cache.h"
#include "content/public/browser/global_request_id.h"
#include "net/base/network_isolation_key.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/mime_util/mime_util.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content {
namespace {
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("interest_group_well_known_fetcher", R"(
semantics {
sender: "Interest group well-known fetcher"
description:
"When a website tries to join or leave an interest group owned "
"by another origin, a .well-known URL needs to be fetched to "
"check if interest group owner has delegated permissions to that "
"site to perform such an operation. "
"See https://github.com/WICG/turtledove/blob/main/FLEDGE.md"
trigger:
"Cross-origin navigator.joinAdInterestGroup() and "
"navigator.leaveAdInterestGroup() calls."
data: "URL registered for updating this interest group."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting:
"Users can disable this via Settings > Privacy and Security > Ads "
"privacy > Site-suggested ads."
chrome_policy {
PrivacySandboxSiteEnabledAdsEnabled {
PrivacySandboxSiteEnabledAdsEnabled: false
}
}
})");
} // namespace
constexpr base::TimeDelta InterestGroupPermissionsChecker::kRequestTimeout =
base::Seconds(30);
InterestGroupPermissionsChecker::ActiveRequest::ActiveRequest() = default;
InterestGroupPermissionsChecker::ActiveRequest::~ActiveRequest() = default;
InterestGroupPermissionsChecker::PendingPermissionsCheck::
PendingPermissionsCheck(Operation operation,
PermissionsCheckCallback permissions_check_callback)
: operation(operation),
permissions_check_callback(std::move(permissions_check_callback)) {}
InterestGroupPermissionsChecker::PendingPermissionsCheck::
PendingPermissionsCheck(PendingPermissionsCheck&&) = default;
InterestGroupPermissionsChecker::PendingPermissionsCheck::
~PendingPermissionsCheck() = default;
InterestGroupPermissionsChecker::InterestGroupPermissionsChecker() = default;
InterestGroupPermissionsChecker::~InterestGroupPermissionsChecker() = default;
void InterestGroupPermissionsChecker::CheckPermissions(
Operation operation,
const url::Origin& frame_origin,
const url::Origin& interest_group_owner,
const net::NetworkIsolationKey& network_isolation_key,
network::mojom::URLLoaderFactory& url_loader_factory,
PermissionsCheckCallback permissions_check_callback) {
// Only HTTPS frames can join or leave interest groups, and only HTTPS origins
// can own interest groups.
DCHECK_EQ(frame_origin.scheme(), url::kHttpsScheme);
DCHECK_EQ(interest_group_owner.scheme(), url::kHttpsScheme);
// Same origin operations are always allowed. Need to invoke callback
// synchronously in this case, so if a page adds an interest group and then
// runs an auction immediately, the interest group is guaranteed to have been
// added before the auction searches for applicable interest groups.
if (frame_origin == interest_group_owner) {
std::move(permissions_check_callback).Run(true);
return;
}
Permissions* permissions = cache_.GetPermissions(
frame_origin, interest_group_owner, network_isolation_key);
if (permissions) {
// If the result is cached, there shouldn't be a pending request for it.
DCHECK_EQ(0u, active_requests_.count({frame_origin, interest_group_owner,
network_isolation_key}));
std::move(permissions_check_callback)
.Run(AllowsOperation(*permissions, operation));
return;
}
PermissionsKey key{frame_origin, interest_group_owner, network_isolation_key};
auto active_request = active_requests_.find(key);
if (active_request == active_requests_.end()) {
active_request = active_requests_
.emplace(std::make_pair(
std::move(key), std::make_unique<ActiveRequest>()))
.first;
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
// These requests are JSON requests made using a URLLoaderFactory matching
// the one created for the renderer process. Therefore, CORS needs to be
// enabled to avoid ORB blocking.
//
// TODO(mmenke): Figure if we really need CORS.
resource_request->mode = network::mojom::RequestMode::kCors;
resource_request->request_initiator = frame_origin;
// Construct .well-known URL.
GURL url = interest_group_owner.GetURL();
GURL::Replacements replacements;
replacements.SetPathStr("/.well-known/interest-group/permissions/");
std::string query = base::StrCat(
{"origin=", base::EscapeQueryParamValue(frame_origin.Serialize(),
/*use_plus=*/false)});
replacements.SetQueryStr(query);
resource_request->url = url.ReplaceComponents(replacements);
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAccept,
"application/json");
active_request->second->simple_url_loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kTrafficAnnotation);
active_request->second->simple_url_loader->SetTimeoutDuration(
kRequestTimeout);
active_request->second->simple_url_loader->SetRequestID(
GlobalRequestID::MakeBrowserInitiated().request_id);
active_request->second->simple_url_loader->DownloadToString(
&url_loader_factory,
base::BindOnce(&InterestGroupPermissionsChecker::OnRequestComplete,
base::Unretained(this), active_request),
kMaxBodySize);
}
active_request->second->pending_checks.emplace_back(PendingPermissionsCheck{
/*operation=*/operation,
/*permissions_check_callback=*/std::move(permissions_check_callback)});
}
void InterestGroupPermissionsChecker::ClearCache() {
cache_.Clear();
}
void InterestGroupPermissionsChecker::OnRequestComplete(
ActiveRequestMap::iterator active_request,
std::unique_ptr<std::string> response_body) {
const auto* response_info =
active_request->second->simple_url_loader->ResponseInfo();
if (!response_body || !response_info ||
!blink::IsJSONMimeType(response_info->mime_type)) {
OnActiveRequestComplete(active_request, Permissions());
return;
}
// `simple_url_loader` is no longer needed after this point.
active_request->second->simple_url_loader.reset();
data_decoder::DataDecoder::ParseJsonIsolated(
*response_body,
base::BindOnce(&InterestGroupPermissionsChecker::OnJsonParsed,
weak_factory_.GetWeakPtr(), active_request));
}
void InterestGroupPermissionsChecker::OnJsonParsed(
ActiveRequestMap::iterator active_request,
data_decoder::DataDecoder::ValueOrError result) {
if (!result.has_value() || !result->is_dict()) {
OnActiveRequestComplete(active_request, Permissions());
return;
}
std::optional<bool> can_join =
result->GetDict().FindBool("joinAdInterestGroup");
std::optional<bool> can_leave =
result->GetDict().FindBool("leaveAdInterestGroup");
Permissions permissions{/*can_join=*/can_join.value_or(false),
/*can_leave=*/can_leave.value_or(false)};
OnActiveRequestComplete(active_request, permissions);
}
void InterestGroupPermissionsChecker::OnActiveRequestComplete(
ActiveRequestMap::iterator active_request,
Permissions permissions) {
// Add permissions to cache, regardless of where they came from (failed
// request, bad response, valid JSON).
cache_.CachePermissions(permissions, active_request->first.frame_origin,
active_request->first.interest_group_owner,
active_request->first.network_isolation_key);
auto pending_checks = std::move(active_request->second->pending_checks);
active_requests_.erase(active_request);
for (auto& pending_check : pending_checks) {
std::move(pending_check.permissions_check_callback)
.Run(AllowsOperation(permissions, pending_check.operation));
}
}
bool InterestGroupPermissionsChecker::AllowsOperation(Permissions permissions,
Operation operation) {
switch (operation) {
case Operation::kJoin:
return permissions.can_join;
case Operation::kLeave:
return permissions.can_leave;
}
}
} // namespace content