blob: ae6b60943cd55c435988663a88702f34edfb8e3d [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 "components/supervised_user/core/browser/kids_external_fetcher.h"
#include <memory>
#include <string>
#include <utility>
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/supervised_user/core/browser/kids_access_token_fetcher.h"
#include "components/supervised_user/core/browser/kids_external_fetcher_config.h"
#include "components/supervised_user/core/browser/kids_external_fetcher_requests.h"
#include "components/supervised_user/core/common/supervised_user_constants.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
namespace {
// Controls the retry count of the simple url loader.
const int kNumFamilyInfoFetcherRetries = 1;
using ::base::BindOnce;
using ::base::JoinString;
using ::base::StrCat;
using ::base::StringPiece;
using ::base::TimeDelta;
using ::base::TimeTicks;
using ::base::UmaHistogramEnumeration;
using ::base::UmaHistogramSparse;
using ::base::UmaHistogramTimes;
using ::base::Unretained;
using ::network::ResourceRequest;
using ::signin::IdentityManager;
bool IsLoadingSuccessful(const network::SimpleURLLoader& loader) {
return loader.NetError() == net::OK;
}
bool HasHttpOkResponse(const network::SimpleURLLoader& loader) {
if (!loader.ResponseInfo()) {
return false;
}
if (!loader.ResponseInfo()->headers) {
return false;
}
return net::HttpStatusCode(loader.ResponseInfo()->headers->response_code()) ==
net::HTTP_OK;
}
int CombineNetAndHttpErrors(const network::SimpleURLLoader& loader) {
if (loader.NetError() != net::OK || !loader.ResponseInfo() ||
!loader.ResponseInfo()->headers) {
return loader.NetError();
}
return loader.ResponseInfo()->headers->response_code();
}
std::string CreateAuthorizationHeader(StringPiece access_token) {
// Do not use StringPiece with StringPrintf, see crbug/1444165
return base::JoinString({supervised_user::kAuthorizationHeader, access_token},
" ");
}
// TODO(b/276898959): Support payload for POST requests.
std::unique_ptr<network::SimpleURLLoader> InitializeSimpleUrlLoader(
StringPiece access_token,
const supervised_user::FetcherConfig& fetcher_config,
const GURL& url) {
std::unique_ptr<ResourceRequest> resource_request =
std::make_unique<ResourceRequest>();
resource_request->url = url;
resource_request->method = fetcher_config.GetHttpMethod();
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
CreateAuthorizationHeader(access_token));
std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
network::SimpleURLLoader::Create(std::move(resource_request),
fetcher_config.traffic_annotation());
simple_url_loader->SetRetryOptions(
kNumFamilyInfoFetcherRetries,
network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
return simple_url_loader;
}
// A fetcher with underlying network::SharedURLLoaderFactory.
// Internally, it's a two-phase process: first the access token is fetched, and
// if applicable, the remote service is called and the response is processed.
template <typename Request, typename Response>
class FetcherImpl final : public KidsExternalFetcher<Request, Response> {
private:
using Callback = typename KidsExternalFetcher<Request, Response>::Callback;
public:
FetcherImpl() = delete;
explicit FetcherImpl(
IdentityManager& identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const supervised_user::FetcherConfig& fetcher_config,
Callback callback)
: config_(fetcher_config) {
access_token_fetcher_ = std::make_unique<KidsAccessTokenFetcher>(
identity_manager,
BindOnce(&FetcherImpl::OnAccessTokenFetchComplete, Unretained(this),
url_loader_factory,
std::move(callback))); // Unretained(.) is safe because `this`
// owns `access_token_fetcher_`.
}
// Not copyable.
FetcherImpl(const FetcherImpl&) = delete;
FetcherImpl& operator=(const FetcherImpl&) = delete;
private:
void RecordStabilityMetrics(TimeDelta latency,
KidsExternalFetcherStatus status) {
UmaHistogramEnumeration(GetMetricKey("Status"), status.state());
UmaHistogramTimes(GetMetricKey("Latency"), latency);
UmaHistogramTimes(GetMetricKey("Latency", status.ToMetricEnumLabel()),
latency);
}
void WrapCallbackWithMetrics(Callback callback,
TimeTicks start_time,
KidsExternalFetcherStatus status,
std::unique_ptr<Response> response) {
TimeDelta latency = TimeTicks::Now() - start_time;
RecordStabilityMetrics(latency, status);
// Record additional metrics for various failures.
if (status.state() == KidsExternalFetcherStatus::State::NET_OR_HTTP_ERROR) {
UmaHistogramSparse(GetMetricKey("NetOrHttpStatus"),
status.net_or_http_error_code().value());
}
DCHECK(
callback); // https://chromium.googlesource.com/chromium/src/+/main/docs/callback.md#creating-a-callback-that-does-nothing
std::move(callback).Run(status, std::move(response));
}
std::string GetMetricKey(base::StringPiece metric_id) const {
return JoinString({config_.histogram_basename, metric_id}, ".");
}
std::string GetMetricKey(base::StringPiece metric_id,
base::StringPiece metric_suffix) const {
return JoinString({config_.histogram_basename, metric_id, metric_suffix},
".");
}
// First phase of fetching done: the access token response is ready.
void OnAccessTokenFetchComplete(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
Callback callback,
base::expected<signin::AccessTokenInfo, GoogleServiceAuthError>
access_token) {
DCHECK(
callback); // https://chromium.googlesource.com/chromium/src/+/main/docs/callback.md#creating-a-callback-that-does-nothing
Callback callback_with_metrics =
BindOnce(&FetcherImpl::WrapCallbackWithMetrics, Unretained(this),
std::move(callback), TimeTicks::Now());
if (!access_token.has_value()) {
std::move(callback_with_metrics)
.Run(KidsExternalFetcherStatus::GoogleServiceAuthError(
access_token.error()),
std::make_unique<Response>());
return;
}
// TODO(b/276898959): add optional payload for POST requests.
simple_url_loader_ = InitializeSimpleUrlLoader(
access_token.value().token, config_,
supervised_user::CreateRequestUrl<Request>(config_));
simple_url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory.get(),
BindOnce(
&FetcherImpl::OnSimpleUrlLoaderComplete, Unretained(this),
std::move(
callback_with_metrics))); // Unretained(.) is safe because
// `this` owns `simple_url_loader_`.
}
// Second phase of fetching done: the remote service responded.
void OnSimpleUrlLoaderComplete(Callback callback,
std::unique_ptr<std::string> response_body) {
if (!IsLoadingSuccessful(*simple_url_loader_) ||
!HasHttpOkResponse(*simple_url_loader_)) {
std::move(callback).Run(KidsExternalFetcherStatus::NetOrHttpError(
CombineNetAndHttpErrors(*simple_url_loader_)),
std::make_unique<Response>());
return;
}
std::unique_ptr<Response> response = std::make_unique<Response>();
if (!response->ParseFromString(*response_body)) {
std::move(callback).Run(KidsExternalFetcherStatus::InvalidResponse(),
std::move(response));
return;
}
std::move(callback).Run(std::move(KidsExternalFetcherStatus::Ok()),
std::move(response));
}
std::unique_ptr<KidsAccessTokenFetcher> access_token_fetcher_;
std::unique_ptr<network::SimpleURLLoader> simple_url_loader_;
const supervised_user::FetcherConfig config_;
};
} // namespace
// Main constructor, referenced by the rest.
KidsExternalFetcherStatus::KidsExternalFetcherStatus(
State state,
class GoogleServiceAuthError google_service_auth_error)
: state_(state), google_service_auth_error_(google_service_auth_error) {}
KidsExternalFetcherStatus::~KidsExternalFetcherStatus() = default;
KidsExternalFetcherStatus::KidsExternalFetcherStatus(State state)
: state_(state) {
DCHECK(state != State::GOOGLE_SERVICE_AUTH_ERROR);
}
KidsExternalFetcherStatus::KidsExternalFetcherStatus(
NetOrHttpErrorType error_code)
: state_(State::NET_OR_HTTP_ERROR), net_or_http_error_code_(error_code) {}
KidsExternalFetcherStatus::KidsExternalFetcherStatus(
class GoogleServiceAuthError google_service_auth_error)
: KidsExternalFetcherStatus(GOOGLE_SERVICE_AUTH_ERROR,
google_service_auth_error) {}
KidsExternalFetcherStatus::KidsExternalFetcherStatus(
const KidsExternalFetcherStatus& other) = default;
KidsExternalFetcherStatus& KidsExternalFetcherStatus::operator=(
const KidsExternalFetcherStatus& other) = default;
KidsExternalFetcherStatus KidsExternalFetcherStatus::Ok() {
return KidsExternalFetcherStatus(State::OK);
}
KidsExternalFetcherStatus KidsExternalFetcherStatus::GoogleServiceAuthError(
class GoogleServiceAuthError error) {
return KidsExternalFetcherStatus(error);
}
KidsExternalFetcherStatus KidsExternalFetcherStatus::NetOrHttpError(
int net_or_http_error_code) {
return KidsExternalFetcherStatus(NetOrHttpErrorType(net_or_http_error_code));
}
KidsExternalFetcherStatus KidsExternalFetcherStatus::InvalidResponse() {
return KidsExternalFetcherStatus(State::INVALID_RESPONSE);
}
KidsExternalFetcherStatus KidsExternalFetcherStatus::DataError() {
return KidsExternalFetcherStatus(State::DATA_ERROR);
}
bool KidsExternalFetcherStatus::IsOk() const {
return state_ == State::OK;
}
bool KidsExternalFetcherStatus::IsTransientError() const {
if (state_ == State::NET_OR_HTTP_ERROR) {
return true;
}
if (state_ == State::GOOGLE_SERVICE_AUTH_ERROR) {
return google_service_auth_error_.IsTransientError();
}
return false;
}
bool KidsExternalFetcherStatus::IsPersistentError() const {
if (state_ == State::INVALID_RESPONSE) {
return true;
}
if (state_ == State::DATA_ERROR) {
return true;
}
if (state_ == State::GOOGLE_SERVICE_AUTH_ERROR) {
return google_service_auth_error_.IsPersistentError();
}
return false;
}
std::string KidsExternalFetcherStatus::ToString() const {
switch (state_) {
case KidsExternalFetcherStatus::OK:
return "KidsExternalFetcherStatus::OK";
case KidsExternalFetcherStatus::GOOGLE_SERVICE_AUTH_ERROR:
return base::StrCat(
{"KidsExternalFetcherStatus::GOOGLE_SERVICE_AUTH_ERROR: ",
google_service_auth_error().ToString()});
case KidsExternalFetcherStatus::NET_OR_HTTP_ERROR:
return base::StringPrintf(
"KidsExternalFetcherStatus::NET_OR_HTTP_ERROR: %d",
net_or_http_error_code_.value());
case KidsExternalFetcherStatus::INVALID_RESPONSE:
return "KidsExternalFetcherStatus::INVALID_RESPONSE";
case KidsExternalFetcherStatus::DATA_ERROR:
return "KidsExternalFetcherStatus::DATA_ERROR";
}
}
// The returned value must match one of the labels in
// chromium/src/tools/metrics/histograms/enums.xml/histogram-configuration/enums/enum[@name='KidsExternalFetcherStatus'],
// and should be reflected in tokens in histogram defined for this fetcher.
// See example at
// chromium/src/tools/metrics/histograms/metadata/signin/histograms.xml/histogram-configuration/histograms/histogram[@name='Signin.ListFamilyMembersRequest.{Status}.*']
std::string KidsExternalFetcherStatus::ToMetricEnumLabel() const {
switch (state_) {
case KidsExternalFetcherStatus::OK:
return "NoError";
case KidsExternalFetcherStatus::GOOGLE_SERVICE_AUTH_ERROR:
return "AuthError";
case KidsExternalFetcherStatus::NET_OR_HTTP_ERROR:
return "HttpError";
case KidsExternalFetcherStatus::INVALID_RESPONSE:
return "ParseError";
case KidsExternalFetcherStatus::DATA_ERROR:
return "DataError";
}
}
KidsExternalFetcherStatus::State KidsExternalFetcherStatus::state() const {
return state_;
}
KidsExternalFetcherStatus::NetOrHttpErrorType
KidsExternalFetcherStatus::net_or_http_error_code() const {
return net_or_http_error_code_;
}
const GoogleServiceAuthError&
KidsExternalFetcherStatus::google_service_auth_error() const {
return google_service_auth_error_;
}
// Fetcher factories.
std::unique_ptr<
KidsExternalFetcher<kids_chrome_management::ListFamilyMembersRequest,
kids_chrome_management::ListFamilyMembersResponse>>
FetchListFamilyMembers(
IdentityManager& identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
KidsExternalFetcher<
kids_chrome_management::ListFamilyMembersRequest,
kids_chrome_management::ListFamilyMembersResponse>::Callback callback,
const supervised_user::FetcherConfig& config) {
return std::make_unique<
FetcherImpl<kids_chrome_management::ListFamilyMembersRequest,
kids_chrome_management::ListFamilyMembersResponse>>(
identity_manager, url_loader_factory, config, std::move(callback));
}