blob: 4b4c09fecba224122eeda2ef9800fbaaa98a34b6 [file] [log] [blame]
// Copyright 2024 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/data_sharing/internal/preview_server_proxy.h"
#include <algorithm>
#include <optional>
#include <utility>
#include "base/base64.h"
#include "base/base64url.h"
#include "base/command_line.h"
#include "base/json/json_reader.h"
#include "base/json/values_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/version_info/channel.h"
#include "components/data_sharing/public/features.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/sync/base/command_line_switches.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/shared_tab_group_data_specifics.pb.h"
#include "google_apis/common/base_requests.h"
#include "net/http/http_request_headers.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/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
using endpoint_fetcher::EndpointFetcher;
using endpoint_fetcher::EndpointResponse;
namespace data_sharing {
namespace {
// RPC timeout duration.
constexpr base::TimeDelta kTimeout = base::Milliseconds(5000);
// Content type for network request.
constexpr char kContentType[] = "application/json; charset=UTF-8";
// OAuth name.
constexpr char kOAuthName[] = "shared_data_preview";
// OAuth scope of the server.
constexpr char kOAuthScope[] = "https://www.googleapis.com/auth/chromesync";
// Server addresses to get preview data.
constexpr char kDefaultServiceBaseUrl[] =
"https://staging-chromesyncsharedentities-pa.googleapis.com/v1";
constexpr char kAutopushServiceBaseUrl[] =
"https://autopush-chromesyncsharedentities-pa.sandbox.googleapis.com/v1";
constexpr char kStableAndBetaServiceBaseUrl[] =
"https://chromesyncsharedentities-pa.googleapis.com/v1";
// How many share entities to retrieve for preview.
constexpr int kDefaultPreviewDataSize = 550;
constexpr base::FeatureParam<int> kPreviewDataSize{
&features::kDataSharingFeature, "preview_data_size",
kDefaultPreviewDataSize};
// lowerCamelCase JSON proto message keys.
constexpr char kSharedEntitiesKey[] = "sharedEntities";
constexpr char kDeletedKey[] = "deleted";
constexpr char kCollaborationKey[] = "collaboration";
constexpr char kCollaborationIdKey[] = "collaborationId";
constexpr char kSpecificsKey[] = "specifics";
constexpr char kSharedGroupDataKey[] = "sharedTabGroupData";
constexpr char kGuidKey[] = "guid";
constexpr char kUpdateTimeWindowsEpochMicrosKey[] =
"updateTimeWindowsEpochMicros";
constexpr char kTabKey[] = "tab";
constexpr char kTabGroupKey[] = "tabGroup";
constexpr char kUrlKey[] = "url";
constexpr char kTitleKey[] = "title";
constexpr char kSharedTabGroupGuidKey[] = "sharedTabGroupGuid";
constexpr char kUniquePositionKey[] = "uniquePosition";
constexpr char kCustomCompressedV1Key[] = "customCompressedV1";
constexpr char kColorKey[] = "color";
constexpr char kGroupVialoationError[] = "SAME_CUSTOMER_DASHER_POLICY_VIOLATED";
struct TabData {
std::string url;
syncer::UniquePosition position;
TabData(const std::string& url, const syncer::UniquePosition& position)
: url(url), position(position) {}
bool operator<(const TabData& other) const {
return position.LessThan(other.position);
}
};
// Network annotation for getting preview data from server.
constexpr net::NetworkTrafficAnnotationTag
kGetSharedDataPreviewTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("chrome_data_sharing_preview",
R"(
semantics {
sender: "Chrome Data Sharing"
description:
"Ask server for a preview of the data shared to a group."
trigger:
"A Chrome-initiated request that requires user enabling the "
"data sharing feature. The request is sent after user receives "
"an invitation link to join a group, and click on a button to "
"get a preview of the data shared to that group."
user_data {
type: OTHER
type: ACCESS_TOKEN
}
data:
"Group ID and access token obtained from the invitation that "
"the user has received."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts { email: "chrome-data-sharing-eng@google.com" }
}
last_reviewed: "2024-08-20"
}
policy {
cookies_allowed: NO
setting:
"This fetch is enabled for any non-enterprise user that has "
"the data sharing feature enabled and is signed in."
chrome_policy {}
}
)");
// Find a a value for a field from a child dictionary in json.
std::optional<std::string> GetFieldValueFromChildDict(
const base::Value::Dict& parent_dict,
const std::string& child_dict_name,
const std::string& field_name) {
auto* child_dict = parent_dict.FindDict(child_dict_name);
if (!child_dict) {
return std::nullopt;
}
auto* field_value = child_dict->FindString(field_name);
if (!field_value) {
return std::nullopt;
}
return *field_value;
}
// Parse the shared tab from the dict.
std::optional<sync_pb::SharedTab> ParseSharedTab(
const base::Value::Dict& dict) {
auto* url = dict.FindString(kUrlKey);
if (!url) {
return std::nullopt;
}
auto* title = dict.FindString(kTitleKey);
auto* shared_tab_group_guid = dict.FindString(kSharedTabGroupGuidKey);
if (!shared_tab_group_guid) {
return std::nullopt;
}
auto custom_compressed = GetFieldValueFromChildDict(dict, kUniquePositionKey,
kCustomCompressedV1Key);
std::optional<sync_pb::SharedTab> shared_tab =
std::make_optional<sync_pb::SharedTab>();
shared_tab->set_url(*url);
if (title) {
shared_tab->set_title(*title);
}
shared_tab->set_shared_tab_group_guid(*shared_tab_group_guid);
if (custom_compressed) {
std::string decoded;
base::Base64Decode(custom_compressed.value(), &decoded);
shared_tab->mutable_unique_position()->set_custom_compressed_v1(decoded);
}
return shared_tab;
}
// Parse the entity specifics from the dict.
std::optional<sync_pb::EntitySpecifics> ParseEntitySpecifics(
const base::Value::Dict& dict) {
auto* shared_tab_group_dict = dict.FindDict(kSharedGroupDataKey);
if (!shared_tab_group_dict) {
return std::nullopt;
}
std::optional<sync_pb::EntitySpecifics> specifics =
std::make_optional<sync_pb::EntitySpecifics>();
sync_pb::SharedTabGroupDataSpecifics* tab_group_data =
specifics->mutable_shared_tab_group_data();
auto* guid = shared_tab_group_dict->FindString(kGuidKey);
if (!guid) {
return std::nullopt;
}
tab_group_data->set_guid(*guid);
auto* update_time_str =
shared_tab_group_dict->FindString(kUpdateTimeWindowsEpochMicrosKey);
uint64_t update_time;
if (update_time_str && base::StringToUint64(*update_time_str, &update_time)) {
tab_group_data->set_update_time_windows_epoch_micros(update_time);
}
auto* tab_dict = shared_tab_group_dict->FindDict(kTabKey);
if (tab_dict) {
auto shared_tab = ParseSharedTab(*tab_dict);
if (shared_tab) {
*(tab_group_data->mutable_tab()) = std::move(shared_tab.value());
} else {
return std::nullopt;
}
} else {
auto* tab_group_dict = shared_tab_group_dict->FindDict(kTabGroupKey);
if (tab_group_dict) {
auto* title = tab_group_dict->FindString(kTitleKey);
if (!title) {
return std::nullopt;
}
sync_pb::SharedTabGroup* shared_tab_group =
tab_group_data->mutable_tab_group();
shared_tab_group->set_title(*title);
auto* color = tab_group_dict->FindString(kColorKey);
if (color) {
sync_pb::SharedTabGroup::Color group_color;
SharedTabGroup_Color_Parse(*color, &group_color);
shared_tab_group->set_color(group_color);
}
} else {
return std::nullopt;
}
}
return specifics;
}
// Deserializes a EntitySpecifics from JSON.
std::optional<sync_pb::EntitySpecifics> Deserialize(const base::Value& value) {
if (!value.is_dict()) {
return std::nullopt;
}
const base::Value::Dict& value_dict = value.GetDict();
// Check if entry is deleted.
auto deleted = value_dict.FindBool(kDeletedKey);
if (deleted.has_value() && deleted.value()) {
return std::nullopt;
}
// Get group id.
auto collaboration_id = GetFieldValueFromChildDict(
value_dict, kCollaborationKey, kCollaborationIdKey);
if (!collaboration_id) {
return std::nullopt;
}
// Get entity specifics.
auto* specifics_dict = value_dict.FindDict(kSpecificsKey);
if (!specifics_dict) {
return std::nullopt;
}
return ParseEntitySpecifics(*specifics_dict);
}
} // namespace
PreviewServerProxy::PreviewServerProxy(
signin::IdentityManager* identity_manager,
const scoped_refptr<network::SharedURLLoaderFactory>& url_loader_factory,
version_info::Channel channel)
: identity_manager_(identity_manager),
url_loader_factory_(url_loader_factory),
channel_(channel) {}
PreviewServerProxy::~PreviewServerProxy() = default;
void PreviewServerProxy::GetSharedDataPreview(
const GroupToken& group_token,
std::optional<syncer::DataType> data_type,
base::OnceCallback<
void(const DataSharingService::SharedDataPreviewOrFailureOutcome&)>
callback) {
if (!group_token.IsValid()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback),
base::unexpected(
DataSharingService::DataPreviewActionFailure::kOtherFailure)));
return;
}
std::string data_type_str;
if (data_type.has_value()) {
int field_number = GetSpecificsFieldNumberFromDataType(*data_type);
data_type_str = base::NumberToString(field_number);
} else {
data_type_str = "-";
}
// Path in the URL to get shared entnties preview, {collaborationId} needs to
// be replaced by the caller.
const char kSharedEntitiesPreviewPath[] =
"collaborations/{collaborationId}/dataTypes/{data_type}/"
"sharedEntities:preview";
std::string shared_entities_preview_path = kSharedEntitiesPreviewPath;
std::string encoded_id;
base::Base64UrlEncode(group_token.group_id.value(),
base::Base64UrlEncodePolicy::OMIT_PADDING, &encoded_id);
base::ReplaceFirstSubstringAfterOffset(&shared_entities_preview_path, 0,
"{collaborationId}", encoded_id);
base::ReplaceFirstSubstringAfterOffset(&shared_entities_preview_path, 0,
"{data_type}", data_type_str);
std::string url_str = GetPreviewServerURLString();
url_str.append("/").append(shared_entities_preview_path);
GURL url = GURL(url_str);
// Query string in the URL to get shared entnties preview. {token} needs to
// be replaced by the caller. {pageSize} can be configured through finch.
const std::string kQueryString =
"accessToken={token}&pageToken=&pageSize={pageSize}";
std::string query_str = kQueryString;
base::ReplaceFirstSubstringAfterOffset(&query_str, 0, "{token}",
group_token.access_token);
base::ReplaceFirstSubstringAfterOffset(
&query_str, 0, "{pageSize}",
base::NumberToString(kPreviewDataSize.Get()));
GURL::Replacements replacements;
replacements.SetQueryStr(query_str);
url = url.ReplaceComponents(replacements);
auto fetcher = CreateEndpointFetcher(url);
auto* const fetcher_ptr = fetcher.get();
fetcher_ptr->Fetch(base::BindOnce(&PreviewServerProxy::HandleServerResponse,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback), std::move(fetcher)));
}
std::unique_ptr<EndpointFetcher> PreviewServerProxy::CreateEndpointFetcher(
const GURL& url) {
return std::make_unique<EndpointFetcher>(
url_loader_factory_, kOAuthName, url, net::HttpRequestHeaders::kGetMethod,
kContentType, std::vector<std::string>{kOAuthScope}, kTimeout,
/* post_data= */ std::string(), kGetSharedDataPreviewTrafficAnnotation,
identity_manager_, signin::ConsentLevel::kSignin);
}
void PreviewServerProxy::HandleServerResponse(
base::OnceCallback<void(
const DataSharingService::SharedDataPreviewOrFailureOutcome&)> callback,
std::unique_ptr<EndpointFetcher> endpoint_fetcher,
std::unique_ptr<EndpointResponse> response) {
if (response->http_status_code != net::HTTP_OK || response->error_type) {
DLOG(ERROR) << "Got bad response (" << response->http_status_code
<< ") for shared data preview!";
DataSharingService::DataPreviewActionFailure failure =
DataSharingService::DataPreviewActionFailure::kOtherFailure;
if (response->http_status_code == net::HTTP_CONFLICT) {
failure = DataSharingService::DataPreviewActionFailure::kGroupFull;
} else if (response->http_status_code == net::HTTP_FORBIDDEN) {
failure = DataSharingService::DataPreviewActionFailure::kPermissionDenied;
std::optional<std::string> reason =
google_apis::MapJsonErrorToReason(response->response);
if (reason.has_value() && reason.value() == kGroupVialoationError) {
failure = DataSharingService::DataPreviewActionFailure::
kGroupClosedByOrganizationPolicy;
}
}
std::move(callback).Run(base::unexpected(failure));
return;
}
data_decoder::DataDecoder::ParseJsonIsolated(
response->response,
base::BindOnce(&PreviewServerProxy::OnResponseJsonParsed,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void PreviewServerProxy::OnResponseJsonParsed(
base::OnceCallback<void(
const DataSharingService::SharedDataPreviewOrFailureOutcome&)> callback,
data_decoder::DataDecoder::ValueOrError result) {
SharedDataPreview preview;
if (result.has_value() && result->is_dict()) {
if (auto* response_json = result->GetDict().FindList(kSharedEntitiesKey)) {
std::optional<SharedTabGroupPreview> group_preview;
std::vector<TabData> tab_data;
for (const auto& shared_entity_json : *response_json) {
if (auto specifics = Deserialize(shared_entity_json)) {
if (specifics && specifics->has_shared_tab_group_data()) {
const sync_pb::SharedTabGroupDataSpecifics& tab_group_data =
specifics->shared_tab_group_data();
if (tab_group_data.has_tab_group()) {
group_preview = SharedTabGroupPreview();
group_preview->title = tab_group_data.tab_group().title();
} else if (tab_group_data.has_tab()) {
tab_data.emplace_back(
tab_group_data.tab().url(),
syncer::UniquePosition::FromProto(
tab_group_data.tab().unique_position()));
}
}
}
}
if (group_preview && !tab_data.empty()) {
// Sort all the tabs.
std::sort(tab_data.begin(), tab_data.end());
for (const auto& data : tab_data) {
group_preview->tabs.emplace_back(GURL(data.url));
}
preview.shared_tab_group_preview = std::move(group_preview);
}
}
}
if (!preview.shared_tab_group_preview) {
std::move(callback).Run(base::unexpected(
DataSharingService::DataPreviewActionFailure::kOtherFailure));
} else {
std::move(callback).Run(std::move(preview));
}
}
std::string PreviewServerProxy::GetPreviewServerURLString() const {
// If there is a param set in a field trial, use the value from that as a
// failsafe.
std::string field_trial_param = GetFieldTrialParamValueByFeature(
features::kDataSharingFeature, "preview_service_base_url");
if (!field_trial_param.empty()) {
return field_trial_param;
}
// If the sync server is set manually (e.g. with
// chrome://flags/#use-sync-sandbox), use autopush.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
syncer::kSyncServiceURL)) {
return kAutopushServiceBaseUrl;
}
// Stable and Beta channel should use the production server.
if (GetChannel() == version_info::Channel::STABLE ||
GetChannel() == version_info::Channel::BETA) {
return kStableAndBetaServiceBaseUrl;
}
// Other channels and local builds use the default service URL.
return kDefaultServiceBaseUrl;
}
version_info::Channel PreviewServerProxy::GetChannel() const {
return channel_;
}
} // namespace data_sharing