blob: 3a8f1ac6855311f8d41167dab014b687121c9378 [file] [log] [blame]
// Copyright 2019 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 "chrome/browser/supervised_user/kids_management_url_checker_client.h"
#include <utility>
#include "base/callback.h"
#include "base/feature_list.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/stl_util.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/values.h"
#include "chrome/browser/supervised_user/supervised_user_constants.h"
#include "components/google/core/common/google_util.h"
#include "google_apis/google_api_keys.h"
#include "net/base/escape.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_request_status.h"
#include "services/identity/public/cpp/primary_account_access_token_fetcher.h"
#include "services/identity/public/cpp/scope_set.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 "url/url_constants.h"
enum class KidsManagementClassification { kAllowed, kRestricted, kUnknown };
enum class KidsManagementResponseStatus {
kSuccess = 0,
kNetworkError = 1,
kUnexpectedClassificationValue = 2,
kParsingError = 3,
kTokenError = 4,
kHttpError = 5,
kMaxValue = kHttpError,
};
struct KidsManagementURLCheckerResponse {
public:
KidsManagementClassification classification;
KidsManagementResponseStatus status;
static KidsManagementURLCheckerResponse ForSuccess(
KidsManagementClassification classification) {
return KidsManagementURLCheckerResponse(
KidsManagementResponseStatus::kSuccess, classification);
}
static KidsManagementURLCheckerResponse ForFailure(
KidsManagementResponseStatus status) {
return KidsManagementURLCheckerResponse(
status, KidsManagementClassification::kUnknown);
}
private:
KidsManagementURLCheckerResponse(
const KidsManagementResponseStatus& status,
const KidsManagementClassification& classification)
: classification(classification), status(status) {}
};
namespace {
const char kClassifyUrlApiPath[] =
"https://kidsmanagement-pa.googleapis.com/kidsmanagement/v1/people/"
"me:classifyUrl";
const char kKidPermissionScope[] =
"https://www.googleapis.com/auth/kid.permission";
const char kDataContentType[] = "application/x-www-form-urlencoded";
const char kDataFormat[] = "url=%s&region_code=%s";
// Possible responses from KidsManagement ClassifyUrl RPC allowed and restricted
const char kAllowed[] = "allowed";
const char kRestricted[] = "restricted";
// Builds the POST data for KidsManagement ClassifyURL requests.
std::string BuildRequestData(const GURL& url, const std::string& region_code) {
std::string query = net::EscapeQueryParamValue(url.spec(), true);
return base::StringPrintf(kDataFormat, query.c_str(), region_code.c_str());
}
// Parses a KidsManagement API |response| and returns the classification.
// On failure returns classification kUnknown and status with the reason for
// failure.
KidsManagementURLCheckerResponse ParseResponse(const std::string& response) {
base::Optional<base::Value> optional_value = base::JSONReader::Read(response);
const base::DictionaryValue* dict = nullptr;
if (!optional_value || !optional_value.value().GetAsDictionary(&dict)) {
DLOG(WARNING) << "ParseResponse failed to parse global dictionary";
return KidsManagementURLCheckerResponse::ForFailure(
KidsManagementResponseStatus::kParsingError);
}
const base::Value* classification_value =
dict->FindKey("displayClassification");
if (!classification_value) {
DLOG(WARNING) << "ParseResponse failed to parse displayClassification";
return KidsManagementURLCheckerResponse::ForFailure(
KidsManagementResponseStatus::kParsingError);
}
const std::string classification_string = classification_value->GetString();
if (classification_string == kAllowed) {
return KidsManagementURLCheckerResponse::ForSuccess(
KidsManagementClassification::kAllowed);
} else if (classification_string == kRestricted) {
return KidsManagementURLCheckerResponse::ForSuccess(
KidsManagementClassification::kRestricted);
}
DLOG(WARNING) << "ParseResponse expected a valid displayClassification";
return KidsManagementURLCheckerResponse::ForFailure(
KidsManagementResponseStatus::kUnexpectedClassificationValue);
}
safe_search_api::ClientClassification ToClientClassification(
const KidsManagementClassification& classification) {
switch (classification) {
case KidsManagementClassification::kAllowed:
return safe_search_api::ClientClassification::kAllowed;
case KidsManagementClassification::kRestricted:
return safe_search_api::ClientClassification::kRestricted;
case KidsManagementClassification::kUnknown:
return safe_search_api::ClientClassification::kUnknown;
}
}
} // namespace
struct KidsManagementURLCheckerClient::Check {
Check(const GURL& url, ClientCheckCallback callback);
~Check();
Check(Check&&) = default;
GURL url;
std::unique_ptr<identity::PrimaryAccountAccessTokenFetcher>
access_token_fetcher;
bool access_token_expired;
std::unique_ptr<network::SimpleURLLoader> simple_url_loader;
ClientCheckCallback callback;
base::TimeTicks start_time;
};
KidsManagementURLCheckerClient::Check::Check(const GURL& url,
ClientCheckCallback callback)
: url(url),
access_token_expired(false),
callback(std::move(callback)),
start_time(base::TimeTicks::Now()) {}
KidsManagementURLCheckerClient::Check::~Check() {
DCHECK(!callback);
}
KidsManagementURLCheckerClient::KidsManagementURLCheckerClient(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& country,
identity::IdentityManager* identity_manager)
: url_loader_factory_(std::move(url_loader_factory)),
traffic_annotation_(
net::DefineNetworkTrafficAnnotation("kids_management_url_checker", R"(
semantics {
sender: "Supervised Users"
description:
"Checks whether a given URL (or set of URLs) is considered safe by "
"Google KidsManagement."
trigger:
"If the parent enabled this feature for the child account, this is "
"sent for every navigation."
data:
"The request is authenticated with an OAuth2 access token "
"identifying the Google account. The URL to be checked."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"This feature is only used in child accounts and cannot be "
"disabled by settings. Parent accounts can disable it in the "
"family dashboard."
chrome_policy {
RestrictSigninToPattern {
policy_options {mode: MANDATORY}
RestrictSigninToPattern: "*@manageddomain.com"
}
}
})")),
country_(country),
identity_manager_(identity_manager) {}
KidsManagementURLCheckerClient::~KidsManagementURLCheckerClient() = default;
void KidsManagementURLCheckerClient::CheckURL(const GURL& url,
ClientCheckCallback callback) {
checks_in_progress_.push_front(
std::make_unique<Check>(url, std::move(callback)));
StartFetching(checks_in_progress_.begin());
}
void KidsManagementURLCheckerClient::StartFetching(CheckList::iterator it) {
identity::ScopeSet scopes{kKidPermissionScope};
// It is safe to use Unretained(this) here given that the callback
// will not be invoked if this object is deleted. Likewise, |it|
// only comes from |checks_in_progress_|, which are owned by this object
// too.
it->get()->access_token_fetcher =
std::make_unique<identity::PrimaryAccountAccessTokenFetcher>(
"kids_url_classifier", identity_manager_, scopes,
base::BindOnce(
&KidsManagementURLCheckerClient::OnAccessTokenFetchComplete,
base::Unretained(this), it),
identity::PrimaryAccountAccessTokenFetcher::Mode::kImmediate);
}
void KidsManagementURLCheckerClient::OnAccessTokenFetchComplete(
CheckList::iterator it,
GoogleServiceAuthError error,
identity::AccessTokenInfo token_info) {
if (error.state() != GoogleServiceAuthError::NONE) {
DLOG(WARNING) << "Token error: " << error.ToString();
DispatchResult(it, KidsManagementURLCheckerResponse::ForFailure(
KidsManagementResponseStatus::kTokenError));
return;
}
Check* check = it->get();
DVLOG(1) << "Checking URL " << check->url;
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GURL(kClassifyUrlApiPath);
resource_request->method = "POST";
resource_request->load_flags =
net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES;
resource_request->headers.SetHeader(
net::HttpRequestHeaders::kAuthorization,
base::StringPrintf(supervised_users::kAuthorizationHeaderFormat,
token_info.token.c_str()));
check->simple_url_loader = network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation_);
check->simple_url_loader->AttachStringForUpload(
BuildRequestData(check->url, country_), kDataContentType);
check->simple_url_loader->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&KidsManagementURLCheckerClient::OnSimpleLoaderComplete,
base::Unretained(this), it, token_info),
/*max_body_size*/ 128);
}
void KidsManagementURLCheckerClient::OnSimpleLoaderComplete(
CheckList::iterator it,
identity::AccessTokenInfo token_info,
std::unique_ptr<std::string> response_body) {
Check* check = it->get();
std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
std::move(check->simple_url_loader);
int response_code = -1;
if (simple_url_loader->ResponseInfo() &&
simple_url_loader->ResponseInfo()->headers) {
response_code = simple_url_loader->ResponseInfo()->headers->response_code();
}
// Handle first HTTP_UNAUTHORIZED response by removing access token and
// restarting the request from the beginning (fetching access token).
if (response_code == net::HTTP_UNAUTHORIZED && !check->access_token_expired) {
DLOG(WARNING) << "Access token expired:\n" << token_info.token;
check->access_token_expired = true;
identity::ScopeSet scopes{kKidPermissionScope};
identity_manager_->RemoveAccessTokenFromCache(
identity_manager_->GetPrimaryAccountId(), scopes, token_info.token);
StartFetching(it);
return;
}
int net_error = simple_url_loader->NetError();
if (net_error != net::OK) {
DLOG(WARNING) << "Network error " << net_error;
DispatchResult(it, KidsManagementURLCheckerResponse::ForFailure(
KidsManagementResponseStatus::kNetworkError));
return;
}
if (response_code != net::HTTP_OK) {
DLOG(WARNING) << "HTTP error " << response_code;
DLOG(WARNING) << "Response: " << response_body.get();
DispatchResult(it, KidsManagementURLCheckerResponse::ForFailure(
KidsManagementResponseStatus::kHttpError));
return;
}
// |response_body| is nullptr only in case of failure
if (!response_body) {
DLOG(WARNING) << "URL request failed! Letting through...";
DispatchResult(it, KidsManagementURLCheckerResponse::ForFailure(
KidsManagementResponseStatus::kNetworkError));
return;
}
DispatchResult(it, ParseResponse(*response_body));
}
void KidsManagementURLCheckerClient::DispatchResult(
CheckList::iterator it,
KidsManagementURLCheckerResponse response) {
if (response.status == KidsManagementResponseStatus::kSuccess) {
UMA_HISTOGRAM_TIMES("ManagedUsers.KidsManagementClassifyUrlSuccessDelay",
base::TimeTicks::Now() - it->get()->start_time);
} else {
UMA_HISTOGRAM_TIMES("ManagedUsers.KidsManagementClassifyUrlFailureDelay",
base::TimeTicks::Now() - it->get()->start_time);
}
UMA_HISTOGRAM_ENUMERATION(
"ManagedUsers.KidsManagementUrlCheckerResponseStatus", response.status);
safe_search_api::ClientClassification api_classification =
ToClientClassification(response.classification);
std::move(it->get()->callback).Run(it->get()->url, api_classification);
checks_in_progress_.erase(it);
}