// 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 "chromeos/ash/components/nearby/presence/credentials/nearby_presence_server_client_impl.h"
#include <memory>
#include "base/base64url.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "chromeos/ash/components/nearby/common/client/nearby_api_call_flow_impl.h"
#include "chromeos/ash/components/nearby/common/client/nearby_http_result.h"
#include "chromeos/ash/components/nearby/presence/proto/list_shared_credentials_rpc.pb.h"
#include "chromeos/ash/components/nearby/presence/proto/rpc_resources.pb.h"
#include "chromeos/ash/components/nearby/presence/proto/update_device_rpc.pb.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "google_apis/gaia/gaia_constants.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace {
// -------------- Nearby Presence Service v1 Endpoints --------------------
const char kDefaultNearbyPresenceV1HTTPHost[] =
"https://nearbypresence-pa.googleapis.com";
const char kNearbyPresenceV1Path[] = "v1/presence/";
const char kListSharedCredentialsPath[] = "listSharedCredentials";
const char kDusi[] = "dusi";
const char kIdentityType[] = "identity_type";
const char kNearbyPresenceOAthConsumerName[] = "nearby_presence_server_client";
// Creates the full Nearby Presence v1 URL for endpoint to the API with
// |request_path|.
GURL CreateV1RequestUrl(const std::string& request_path) {
GURL google_apis_url = GURL(kDefaultNearbyPresenceV1HTTPHost);
return google_apis_url.Resolve(kNearbyPresenceV1Path + request_path);
}
ash::nearby::NearbyApiCallFlow::QueryParameters
ListSharedCredentialsRequestToQueryParameters(
const ash::nearby::proto::ListSharedCredentialsRequest& request) {
ash::nearby::NearbyApiCallFlow::QueryParameters query_parameters;
query_parameters.emplace_back(kDusi, request.dusi());
query_parameters.emplace_back(kIdentityType,
base::NumberToString(request.identity_type()));
return query_parameters;
}
const net::PartialNetworkTrafficAnnotationTag& GetUpdateDeviceAnnotation() {
static const net::PartialNetworkTrafficAnnotationTag annotation =
net::DefinePartialNetworkTrafficAnnotation(
"nearby_presence_update_device", "oauth2_api_call_flow",
R"(
semantics {
sender: "Nearby Presence"
description:
"Used as part of the Nearby Presence feature that allows a scanning "
"abstraction layer, and a device identity management library. "
"The call sends the local device's user-defined name and "
"Nearby Presence specific crypto data from the local device to the "
"Google-owned Nearby server. This data is also uploaded to the "
"server and distributed to other devices in the user GAIA to help "
"establish an authenticated channel during the Nearby Presence flow. "
"This crypto data can be immediately invalidated by the local device "
"at any time without needing to communicate with the server. For "
"example, it expires after five days and new data needs to be "
"uploaded. The server returns the local device user's full name and "
"icon URL if available on the Google server."
trigger:
"Automatically called daily to retrieve any updates to the user's "
"full name or icon URL. This request is also sent whenever new "
"crypto data is generated by the local device and needs to be "
"shared with other devices in the user GAIA. This request is also "
"sent when a new device is added to the user GAIA and needs the "
"local device's crypto data."
data:
"Sends an OAuth 2.0 token, the local device's name, contact, and/or "
"Nearby Presence specific crypto data. Possibly receives the user's "
"full name and icon URL from the Google server."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
email: "[email protected]"
}
contacts {
email: "[email protected]"
}
}
user_data {
type: ACCESS_TOKEN
type: ARBITRARY_DATA
}
last_reviewed: "2023-04-04"
}
policy {
setting:
"Only sent when Nearby Presence is enabled and the user is signed in "
"with their Google account."
chrome_policy {
SigninAllowed {
SigninAllowed: false
}
}
})");
return annotation;
}
const net::PartialNetworkTrafficAnnotationTag&
GetListSharedCredentialsAnnotation() {
static const net::PartialNetworkTrafficAnnotationTag annotation =
net::DefinePartialNetworkTrafficAnnotation(
"nearby_presence_list_shared_credentials", "oauth2_api_call_flow",
R"(
semantics {
sender: "Nearby Presence"
description:
"Used as part of the Nearby Presence feature that allows a scanning "
"abstraction layer, and a device identity management library. "
"The call retrieves Nearby Presence specific crypto data "
"from the Google-owned Nearby server. The data was uploaded by other "
"devices and is needed to establish an authenticated connection with "
"those device during the Nearby Presence flow."
trigger:
"Automatically called at least once a day to retrieve any updates to "
"the list of crypto data. It is also called when Nearby Presence is "
"in use to ensure up-to-date data."
data:
"Sends an OAuth 2.0 token. Receives Nearby Presence specific crypto "
"necessary for establishing an authenticated channel with other "
"devices."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
email: "[email protected]"
}
contacts {
email: "[email protected]"
}
}
user_data {
type: ACCESS_TOKEN
type: ARBITRARY_DATA
}
last_reviewed: "2023-04-04"
}
policy {
setting:
"Only sent when Nearby Presence is enabled and the user is signed in "
"with their Google account."
chrome_policy {
SigninAllowed {
SigninAllowed: false
}
}
})");
return annotation;
}
} // namespace
namespace ash::nearby::presence {
// static
NearbyPresenceServerClientImpl::Factory*
NearbyPresenceServerClientImpl::Factory::g_test_factory_ = nullptr;
// static
std::unique_ptr<NearbyPresenceServerClient>
NearbyPresenceServerClientImpl::Factory::Create(
std::unique_ptr<NearbyApiCallFlow> api_call_flow,
signin::IdentityManager* identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
if (g_test_factory_) {
return g_test_factory_->CreateInstance(std::move(api_call_flow),
identity_manager,
std::move(url_loader_factory));
}
return base::WrapUnique(new NearbyPresenceServerClientImpl(
std::move(api_call_flow), identity_manager,
std::move(url_loader_factory)));
}
// static
void NearbyPresenceServerClientImpl::Factory::SetFactoryForTesting(
Factory* test_factory) {
g_test_factory_ = test_factory;
}
NearbyPresenceServerClientImpl::Factory::~Factory() = default;
NearbyPresenceServerClientImpl::NearbyPresenceServerClientImpl(
std::unique_ptr<NearbyApiCallFlow> api_call_flow,
signin::IdentityManager* identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: api_call_flow_(std::move(api_call_flow)),
identity_manager_(identity_manager),
url_loader_factory_(std::move(url_loader_factory)) {}
NearbyPresenceServerClientImpl::~NearbyPresenceServerClientImpl() = default;
void NearbyPresenceServerClientImpl::UpdateDevice(
const ash::nearby::proto::UpdateDeviceRequest& request,
UpdateDeviceCallback callback,
ErrorCallback error_callback) {
MakeApiCall(CreateV1RequestUrl(request.device().name()), RequestType::kPatch,
request.SerializeAsString(),
/*request_as_query_parameters=*/std::nullopt, std::move(callback),
std::move(error_callback), GetUpdateDeviceAnnotation());
}
void NearbyPresenceServerClientImpl::ListSharedCredentials(
const ash::nearby::proto::ListSharedCredentialsRequest& request,
ListSharedCredentialsCallback callback,
ErrorCallback error_callback) {
MakeApiCall(CreateV1RequestUrl(request.dusi() + "/" +
base::NumberToString(request.identity_type()) +
"/" + kListSharedCredentialsPath),
RequestType::kGet, /*serialized_request=*/std::nullopt,
ListSharedCredentialsRequestToQueryParameters(request),
std::move(callback), std::move(error_callback),
GetListSharedCredentialsAnnotation());
}
std::string NearbyPresenceServerClientImpl::GetAccessTokenUsed() {
return access_token_used_;
}
template <class ResponseProto>
void NearbyPresenceServerClientImpl::MakeApiCall(
const GURL& request_url,
RequestType request_type,
const std::optional<std::string>& serialized_request,
const std::optional<NearbyApiCallFlow::QueryParameters>&
request_as_query_parameters,
base::OnceCallback<void(const ResponseProto&)> response_callback,
ErrorCallback error_callback,
const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) {
DCHECK(!has_call_started_)
<< "NearbyPresenceServerClientImpl::MakeApiCall(): Tried to make an API "
<< "call, but the client had already been used.";
has_call_started_ = true;
api_call_flow_->SetPartialNetworkTrafficAnnotation(
partial_traffic_annotation);
request_url_ = request_url;
error_callback_ = std::move(error_callback);
OAuth2AccessTokenManager::ScopeSet scopes;
scopes.insert(GaiaConstants::kNearbyPresenceOAuth2Scope);
access_token_fetcher_ = std::make_unique<
signin::PrimaryAccountAccessTokenFetcher>(
kNearbyPresenceOAthConsumerName, identity_manager_, scopes,
base::BindOnce(
&NearbyPresenceServerClientImpl::OnAccessTokenFetched<ResponseProto>,
weak_ptr_factory_.GetWeakPtr(), request_type, serialized_request,
request_as_query_parameters, std::move(response_callback)),
signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable,
signin::ConsentLevel::kSignin);
}
template <class ResponseProto>
void NearbyPresenceServerClientImpl::OnAccessTokenFetched(
RequestType request_type,
const std::optional<std::string>& serialized_request,
const std::optional<NearbyApiCallFlow::QueryParameters>&
request_as_query_parameters,
base::OnceCallback<void(const ResponseProto&)> response_callback,
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
access_token_fetcher_.reset();
if (error.state() != GoogleServiceAuthError::NONE) {
OnApiCallFailed(NearbyHttpError::kAuthenticationError);
return;
}
access_token_used_ = access_token_info.token;
switch (request_type) {
case RequestType::kGet:
DCHECK(request_as_query_parameters && !serialized_request);
api_call_flow_->StartGetRequest(
request_url_, *request_as_query_parameters, url_loader_factory_,
access_token_used_,
base::BindOnce(
&NearbyPresenceServerClientImpl::OnFlowSuccess<ResponseProto>,
weak_ptr_factory_.GetWeakPtr(), std::move(response_callback)),
base::BindOnce(&NearbyPresenceServerClientImpl::OnApiCallFailed,
weak_ptr_factory_.GetWeakPtr()));
break;
case RequestType::kPost:
DCHECK(serialized_request && !request_as_query_parameters);
api_call_flow_->StartPostRequest(
request_url_, *serialized_request, url_loader_factory_,
access_token_used_,
base::BindOnce(
&NearbyPresenceServerClientImpl::OnFlowSuccess<ResponseProto>,
weak_ptr_factory_.GetWeakPtr(), std::move(response_callback)),
base::BindOnce(&NearbyPresenceServerClientImpl::OnApiCallFailed,
weak_ptr_factory_.GetWeakPtr()));
break;
case RequestType::kPatch:
DCHECK(serialized_request && !request_as_query_parameters);
api_call_flow_->StartPatchRequest(
request_url_, *serialized_request, url_loader_factory_,
access_token_used_,
base::BindOnce(
&NearbyPresenceServerClientImpl::OnFlowSuccess<ResponseProto>,
weak_ptr_factory_.GetWeakPtr(), std::move(response_callback)),
base::BindOnce(&NearbyPresenceServerClientImpl::OnApiCallFailed,
weak_ptr_factory_.GetWeakPtr()));
break;
}
}
template <class ResponseProto>
void NearbyPresenceServerClientImpl::OnFlowSuccess(
base::OnceCallback<void(const ResponseProto&)> result_callback,
const std::string& serialized_response) {
ResponseProto response;
if (!response.ParseFromString(serialized_response)) {
OnApiCallFailed(NearbyHttpError::kResponseMalformed);
return;
}
std::move(result_callback).Run(response);
}
void NearbyPresenceServerClientImpl::OnApiCallFailed(NearbyHttpError error) {
std::move(error_callback_).Run(error);
}
} // namespace ash::nearby::presence