blob: 2a8f17621f782d8dfe410913ee8a7c604c13fd2a [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/plus_addresses/plus_address_client.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "components/plus_addresses/features.h"
#include "components/plus_addresses/plus_address_metrics.h"
#include "components/plus_addresses/plus_address_parser.h"
#include "components/plus_addresses/plus_address_types.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "components/signin/public/identity_manager/scope_set.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/gurl.h"
namespace plus_addresses {
namespace {
const base::TimeDelta kRequestTimeout = base::Seconds(5);
// See docs/network_traffic_annotations.md for reference.
// TODO(b/295556954): Update the description and trigger fields when possible.
// Also replace the policy_exception when we have a policy.
const net::NetworkTrafficAnnotationTag kCreatePlusAddressAnnotation =
net::DefineNetworkTrafficAnnotation("plus_address_creation", R"(
semantics {
sender: "Chrome Plus Address Client"
description: "A plus address is created on the enterprise-specified "
"server with this request."
trigger: "User chooses to create a plus address."
internal {
contacts {
email: "dc-komics@google.com"
}
}
user_data {
type: ACCESS_TOKEN,
type: SENSITIVE_URL
}
data: "The origin on which the user wants to use a plus address is "
"sent."
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-09-07"
}
policy {
cookies_allowed: NO
setting: "Disable the Plus Addresses feature."
policy_exception_justification: "We don't have an opt-out policy yet"
" as Plus Addresses hasn't launched."
}
)");
// TODO(b/295556954): Update the description and trigger fields when possible.
// Also replace the policy_exception when we have a policy.
const net::NetworkTrafficAnnotationTag kReservePlusAddressAnnotation =
net::DefineNetworkTrafficAnnotation("plus_address_reservation", R"(
semantics {
sender: "Chrome Plus Address Client"
description: "A plus address is reserved for the user on the "
"enterprise-specified server with this request."
trigger: "User enters the create plus address UX flow."
internal {
contacts {
email: "dc-komics@google.com"
}
}
user_data {
type: ACCESS_TOKEN,
type: SENSITIVE_URL
}
data: "The origin that the user may use a plus address on is sent."
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-09-23"
}
policy {
cookies_allowed: NO
setting: "Disable the Plus Addresses feature."
policy_exception_justification: "We don't have an opt-out policy yet"
" as Plus Addresses hasn't launched."
}
)");
// TODO(b/277532955): Update the description and trigger fields when possible.
// Also replace the policy_exception when we have a policy.
const net::NetworkTrafficAnnotationTag kConfirmPlusAddressAnnotation =
net::DefineNetworkTrafficAnnotation("plus_address_confirmation", R"(
semantics {
sender: "Chrome Plus Address Client"
description: "A plus address is confirmed for creation on the "
"enterprise-specified server with this request."
trigger: "User confirms to create the displayed plus address."
internal {
contacts {
email: "dc-komics@google.com"
}
}
user_data {
type: ACCESS_TOKEN,
type: SENSITIVE_URL,
type: USERNAME
}
data: "The plus address and the origin that the user is using it on "
"are both sent."
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-09-23"
}
policy {
cookies_allowed: NO
setting: "Disable the Plus Addresses feature."
policy_exception_justification: "We don't have an opt-out policy yet"
" as Plus Addresses hasn't launched."
}
)");
// TODO(b/295556954): Update the description and trigger fields when possible.
// Also replace the policy_exception when we have a policy.
const net::NetworkTrafficAnnotationTag kGetAllPlusAddressesAnnotation =
net::DefineNetworkTrafficAnnotation("get_all_plus_addresses", R"(
semantics {
sender: "Chrome Plus Address Client"
description: "This request fetches all plus addresses from the "
"enterprise-specified server."
trigger: "n/a. This happens in the background to keep the PlusAddress "
"service in sync with the remote server."
internal {
contacts {
email: "dc-komics@google.com"
}
}
user_data {
type: ACCESS_TOKEN
}
data: "n/a"
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-09-13"
}
policy {
cookies_allowed: NO
setting: "Disable the Plus Addresses feature."
policy_exception_justification: "We don't have an opt-out policy yet"
" as Plus Addresses hasn't launched."
}
)");
absl::optional<GURL> ValidateAndGetUrl() {
GURL maybe_url = GURL(kEnterprisePlusAddressServerUrl.Get());
return maybe_url.is_valid() ? absl::make_optional(maybe_url) : absl::nullopt;
}
} // namespace
PlusAddressClient::PlusAddressClient(
signin::IdentityManager* identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: identity_manager_(identity_manager),
url_loader_factory_(std::move(url_loader_factory)),
server_url_(ValidateAndGetUrl()),
scopes_({kEnterprisePlusAddressOAuthScope.Get()}) {}
PlusAddressClient::~PlusAddressClient() = default;
PlusAddressClient::PlusAddressClient(PlusAddressClient&&) = default;
PlusAddressClient& PlusAddressClient::operator=(PlusAddressClient&&) = default;
void PlusAddressClient::CreatePlusAddress(const url::Origin& origin,
PlusAddressCallback callback) {
if (!server_url_) {
return;
}
// Refresh the OAuth token if it's expired.
if (access_token_info_.expiration_time < clock_->Now()) {
GetAuthToken(base::BindOnce(&PlusAddressClient::CreatePlusAddress,
base::Unretained(this), origin,
std::move(callback)));
return;
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->method = net::HttpRequestHeaders::kPutMethod;
resource_request->url =
server_url_.value().Resolve(kServerPlusProfileEndpoint);
resource_request->headers.SetHeader(
net::HttpRequestHeaders::kAuthorization,
base::StrCat({"Bearer ", access_token_info_.token}));
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
base::Value::Dict payload;
payload.Set("facet", origin.Serialize());
std::string request_body;
bool wrote_payload = base::JSONWriter::Write(payload, &request_body);
DCHECK(wrote_payload);
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kCreatePlusAddressAnnotation);
network::SimpleURLLoader* loader_ptr = loader.get();
loader_ptr->AttachStringForUpload(request_body, "application/json");
loader_ptr->SetTimeoutDuration(kRequestTimeout);
// TODO(b/301984623) - Measure average downloadsize and change this.
loader_ptr->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&PlusAddressClient::OnCreatePlusAddressComplete,
// Safe since this class owns the list of loaders.
base::Unretained(this),
loaders_for_creation_.insert(loaders_for_creation_.begin(),
std::move(loader)),
clock_->Now(), std::move(callback)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void PlusAddressClient::ReservePlusAddress(
const url::Origin& origin,
PlusAddressRequestCallback on_completed) {
if (!server_url_) {
return;
}
// Refresh the OAuth token if it's expired.
if (access_token_info_.expiration_time < clock_->Now()) {
GetAuthToken(base::BindOnce(&PlusAddressClient::ReservePlusAddress,
base::Unretained(this), origin,
std::move(on_completed)));
return;
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->method = net::HttpRequestHeaders::kPutMethod;
resource_request->url =
server_url_.value().Resolve(kServerReservePlusAddressEndpoint);
resource_request->headers.SetHeader(
net::HttpRequestHeaders::kAuthorization,
base::StrCat({"Bearer ", access_token_info_.token}));
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
base::Value::Dict payload;
payload.Set("facet", origin.Serialize());
std::string request_body;
bool wrote_payload = base::JSONWriter::Write(payload, &request_body);
DCHECK(wrote_payload);
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kReservePlusAddressAnnotation);
network::SimpleURLLoader* loader_ptr = loader.get();
loader_ptr->AttachStringForUpload(request_body, "application/json");
loader_ptr->SetTimeoutDuration(kRequestTimeout);
// TODO(b/301984623) - Measure average downloadsize and change this.
loader_ptr->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&PlusAddressClient::OnReserveOrConfirmPlusAddressComplete,
// Safe since this class owns the list of loaders.
base::Unretained(this),
loaders_for_creation_.insert(loaders_for_creation_.begin(),
std::move(loader)),
PlusAddressNetworkRequestType::kReserve, clock_->Now(),
std::move(on_completed)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void PlusAddressClient::ConfirmPlusAddress(
const url::Origin& origin,
const std::string& plus_address,
PlusAddressRequestCallback on_completed) {
if (!server_url_) {
return;
}
// Refresh the OAuth token if it's expired.
if (access_token_info_.expiration_time < clock_->Now()) {
GetAuthToken(base::BindOnce(&PlusAddressClient::ConfirmPlusAddress,
base::Unretained(this), origin, plus_address,
std::move(on_completed)));
return;
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->method = net::HttpRequestHeaders::kPutMethod;
resource_request->url =
server_url_.value().Resolve(kServerCreatePlusAddressEndpoint);
resource_request->headers.SetHeader(
net::HttpRequestHeaders::kAuthorization,
base::StrCat({"Bearer ", access_token_info_.token}));
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
base::Value::Dict payload;
payload.Set("facet", origin.Serialize());
payload.Set("reserved_email_address", plus_address);
std::string request_body;
bool wrote_payload = base::JSONWriter::Write(payload, &request_body);
DCHECK(wrote_payload);
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kConfirmPlusAddressAnnotation);
network::SimpleURLLoader* loader_ptr = loader.get();
loader_ptr->AttachStringForUpload(request_body, "application/json");
loader_ptr->SetTimeoutDuration(kRequestTimeout);
// TODO(b/301984623) - Measure average downloadsize and change this.
loader_ptr->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&PlusAddressClient::OnReserveOrConfirmPlusAddressComplete,
// Safe since this class owns the list of loaders.
base::Unretained(this),
loaders_for_creation_.insert(loaders_for_creation_.begin(),
std::move(loader)),
PlusAddressNetworkRequestType::kCreate, clock_->Now(),
std::move(on_completed)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void PlusAddressClient::GetAllPlusAddresses(PlusAddressMapCallback callback) {
if (!server_url_) {
return;
}
// Refresh the OAuth token if it's expired.
if (access_token_info_.expiration_time < clock_->Now()) {
GetAuthToken(base::BindOnce(&PlusAddressClient::GetAllPlusAddresses,
base::Unretained(this), std::move(callback)));
return;
}
// Fail early if the URL Loader is already in-use. We never expect this method
// to be called in quick succession.
if (loader_for_sync_) {
DCHECK(false);
return;
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->method = net::HttpRequestHeaders::kGetMethod;
resource_request->url =
server_url_.value().Resolve(kServerPlusProfileEndpoint);
resource_request->headers.SetHeader(
net::HttpRequestHeaders::kAuthorization,
base::StrCat({"Bearer ", access_token_info_.token}));
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
loader_for_sync_ = network::SimpleURLLoader::Create(
std::move(resource_request), kGetAllPlusAddressesAnnotation);
loader_for_sync_->SetTimeoutDuration(kRequestTimeout);
loader_for_sync_->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&PlusAddressClient::OnGetAllPlusAddressesComplete,
// Safe since this class owns the loader_for_sync_.
base::Unretained(this), clock_->Now(),
std::move(callback)),
// TODO(b/301984623) - Measure average downloadsize and change this.
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void PlusAddressClient::OnCreatePlusAddressComplete(
UrlLoaderList::iterator it,
base::Time request_start,
PlusAddressCallback callback,
std::unique_ptr<std::string> response) {
// Record relevant metrics.
std::unique_ptr<network::SimpleURLLoader> loader = std::move(*it);
PlusAddressMetrics::RecordNetworkRequestLatency(
PlusAddressNetworkRequestType::kGetOrCreate,
clock_->Now() - request_start);
if (loader && loader->ResponseInfo() && loader->ResponseInfo()->headers) {
PlusAddressMetrics::RecordNetworkRequestResponseCode(
PlusAddressNetworkRequestType::kGetOrCreate,
loader->ResponseInfo()->headers->response_code());
}
// Destroy the loader before returning.
loaders_for_creation_.erase(it);
if (!response) {
return;
}
PlusAddressMetrics::RecordNetworkRequestResponseSize(
PlusAddressNetworkRequestType::kGetOrCreate, response->size());
// Parse the response & return it via callback.
data_decoder::DataDecoder::ParseJsonIsolated(
*response,
base::BindOnce(&PlusAddressParser::ParsePlusProfileFromV1Create)
.Then(base::BindOnce(
[](PlusAddressCallback callback,
absl::optional<PlusProfile> result) {
if (result.has_value()) {
std::move(callback).Run(result->plus_address);
}
},
std::move(callback))));
}
void PlusAddressClient::OnReserveOrConfirmPlusAddressComplete(
UrlLoaderList::iterator it,
PlusAddressNetworkRequestType type,
base::Time request_start,
PlusAddressRequestCallback on_completed,
std::unique_ptr<std::string> response) {
// Record relevant metrics.
std::unique_ptr<network::SimpleURLLoader> loader = std::move(*it);
PlusAddressMetrics::RecordNetworkRequestLatency(
type, clock_->Now() - request_start);
if (loader && loader->ResponseInfo() && loader->ResponseInfo()->headers) {
PlusAddressMetrics::RecordNetworkRequestResponseCode(
type, loader->ResponseInfo()->headers->response_code());
}
// Destroy the loader before returning.
loaders_for_creation_.erase(it);
if (!response) {
std::move(on_completed)
.Run(base::unexpected(PlusAddressRequestError(
PlusAddressRequestErrorType::kNetworkError)));
return;
}
PlusAddressMetrics::RecordNetworkRequestResponseSize(type, response->size());
// Parse the response & return it via callback.
data_decoder::DataDecoder::ParseJsonIsolated(
*response,
base::BindOnce(&PlusAddressParser::ParsePlusProfileFromV1Create)
.Then(base::BindOnce(
[](PlusAddressRequestCallback callback,
absl::optional<PlusProfile> result) {
if (!result.has_value()) {
std::move(callback).Run(
base::unexpected(PlusAddressRequestError(
PlusAddressRequestErrorType::kParsingError)));
return;
}
std::move(callback).Run(result.value());
},
std::move(on_completed))));
}
void PlusAddressClient::OnGetAllPlusAddressesComplete(
base::Time request_start,
PlusAddressMapCallback callback,
std::unique_ptr<std::string> response) {
// Record relevant metrics.
PlusAddressMetrics::RecordNetworkRequestLatency(
PlusAddressNetworkRequestType::kList, clock_->Now() - request_start);
if (loader_for_sync_ && loader_for_sync_->ResponseInfo() &&
loader_for_sync_->ResponseInfo()->headers) {
PlusAddressMetrics::RecordNetworkRequestResponseCode(
PlusAddressNetworkRequestType::kList,
loader_for_sync_->ResponseInfo()->headers->response_code());
}
// Destroy the loader before returning.
loader_for_sync_.reset();
if (!response) {
return;
}
PlusAddressMetrics::RecordNetworkRequestResponseSize(
PlusAddressNetworkRequestType::kList, response->size());
// Parse the response & return it via callback.
data_decoder::DataDecoder::ParseJsonIsolated(
*response,
base::BindOnce(&PlusAddressParser::ParsePlusAddressMapFromV1List)
.Then(base::BindOnce(
[](PlusAddressMapCallback callback,
absl::optional<PlusAddressMap> result) {
if (result.has_value()) {
std::move(callback).Run(result.value());
}
},
std::move(callback))));
}
// TODO (kaklilu): Handle requests when token is nearing expiration.
void PlusAddressClient::GetAuthToken(base::OnceClosure callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(access_token_info_.expiration_time < clock_->Now());
// Enqueue `callback` to be run after the token is fetched.
pending_callbacks_.emplace(std::move(callback));
if (!access_token_fetcher_) {
// Only request an auth token if it's not yet pending.
RequestAuthToken();
}
}
void PlusAddressClient::RequestAuthToken() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
access_token_fetcher_ =
std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
/*consumer_name=*/"PlusAddressClient", identity_manager_, scopes_,
base::BindOnce(&PlusAddressClient::OnTokenFetched,
// It is safe to use base::Unretained as
// |this| owns |access_token_fetcher_|.
base::Unretained(this)),
// Use WaitUntilAvailable to defer getting an OAuth token until
// the user is signed in. We can switch to kImmediate once we
// have a sign in observer that guarantees we're already signed in
// by this point.
signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable,
// Sync doesn't need to be enabled for us to use PlusAddresses.
signin::ConsentLevel::kSignin);
}
void PlusAddressClient::OnTokenFetched(
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
access_token_fetcher_.reset();
PlusAddressMetrics::RecordNetworkRequestOauthError(error);
if (error.state() == GoogleServiceAuthError::NONE) {
access_token_info_ = access_token_info;
// Run stored callbacks.
while (!pending_callbacks_.empty()) {
std::move(pending_callbacks_.front()).Run();
pending_callbacks_.pop();
}
} else {
access_token_request_error_ = error;
VLOG(1) << "PlusAddressClient failed to get OAuth token:"
<< error.ToString();
}
}
} // namespace plus_addresses