chromium/chrome/browser/push_notification/server_client/push_notification_server_client_desktop_impl.cc

// 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 "chrome/browser/push_notification/server_client/push_notification_server_client_desktop_impl.h"

#include "base/base64url.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/push_notification/metrics/push_notification_metrics.h"
#include "chrome/browser/push_notification/protos/notifications_multi_login_update.pb.h"
#include "chrome/browser/push_notification/server_client/push_notification_desktop_api_call_flow_impl.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 "services/network/public/cpp/shared_url_loader_factory.h"

namespace {

// Push Notification Service v1 Endpoints
const char kDefaultPushNotificationV1HTTPHost[] =
    "https://notifications-pa.googleapis.com";

const char kPushNotificationV1Path[] = "v1/";

const char kMultiLoginUpdatePath[] = "multiloginupdate";

const char kPushNotificationOath2Scope[] =
    "https://www.googleapis.com/auth/notifications";

const char kPushNotificationOAuthName[] = "push_notification_client";

// Creates the full Push Notification v1 URL for endpoint to the API with
// |request_path|.
GURL CreateV1RequestUrl(const std::string& request_path) {
  GURL google_apis_url = GURL(kDefaultPushNotificationV1HTTPHost);
  return google_apis_url.Resolve(kPushNotificationV1Path + request_path);
}

const net::PartialNetworkTrafficAnnotationTag&
GetRegisterPushNotificationServiceAnnotation() {
  static const net::PartialNetworkTrafficAnnotationTag annotation =
      net::DefinePartialNetworkTrafficAnnotation(
          "register_push_notification_service", "oauth2_api_call_flow",
          R"(
      semantics {
        sender: "Chrome Desktop Push Notification Service"
        description:
          "Registers the Chrome Desktop Push Notification Service"
          "with the MultiLoginUpdate API of the Chime Service. "
          "the call sends an OAuth 2.0 token and GCM token"
        trigger:
          "Automatically called during initialization of the  "
          "Chrome Desktop Push Notification Service "
        data:
          "Sends an OAuth 2.0 token and GCM token"
        destination: GOOGLE_OWNED_SERVICE
        internal {
            contacts {
              email: "[email protected]"
            }
            contacts {
              email: "[email protected]"
            }
          }
          user_data {
            type: ACCESS_TOKEN
            type: ARBITRARY_DATA
          }
          last_reviewed: "2024-04-29"
      }
      policy {
        setting:
          "Only sent when Chrome Desktop Push Notification Service"
          "is enabled and the user is signed in with their Google account."
        chrome_policy {
          SigninAllowed {
            SigninAllowed: false
          }
        }
      })");
  return annotation;
}

}  // namespace

namespace push_notification {

// static
PushNotificationServerClientDesktopImpl::Factory*
    PushNotificationServerClientDesktopImpl::Factory::g_test_factory_ = nullptr;

// static
std::unique_ptr<PushNotificationServerClient>
PushNotificationServerClientDesktopImpl::Factory::Create(
    std::unique_ptr<PushNotificationDesktopApiCallFlow> 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 PushNotificationServerClientDesktopImpl(
      std::move(api_call_flow), identity_manager,
      std::move(url_loader_factory)));
}

// static
void PushNotificationServerClientDesktopImpl::Factory::SetFactoryForTesting(
    Factory* test_factory) {
  g_test_factory_ = test_factory;
}

PushNotificationServerClientDesktopImpl::Factory::~Factory() = default;

PushNotificationServerClientDesktopImpl::
    PushNotificationServerClientDesktopImpl(
        std::unique_ptr<PushNotificationDesktopApiCallFlow> api_call_flow,
        signin::IdentityManager* identity_manager,
        scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
    : api_call_flow_(std::move(api_call_flow)),
      url_loader_factory_(std::move(url_loader_factory)),
      has_call_started_(false),
      identity_manager_(identity_manager) {}

PushNotificationServerClientDesktopImpl::
    ~PushNotificationServerClientDesktopImpl() = default;

void PushNotificationServerClientDesktopImpl::
    RegisterWithPushNotificationService(
        const proto::NotificationsMultiLoginUpdateRequest& request,
        RegisterWithPushNotificationServiceCallback&& callback,
        ErrorCallback&& error_callback) {
  MakeApiCall(CreateV1RequestUrl(kMultiLoginUpdatePath), RequestType::kPost,
              request.SerializeAsString(),
              /*request_as_query_parameters=*/std::nullopt, std::move(callback),
              std::move(error_callback),
              GetRegisterPushNotificationServiceAnnotation());
}

std::optional<std::string>
PushNotificationServerClientDesktopImpl::GetAccessTokenUsed() {
  return access_token_used_;
}

template <class ResponseProto>
void PushNotificationServerClientDesktopImpl::MakeApiCall(
    const GURL& request_url,
    RequestType request_type,
    const std::optional<std::string>& serialized_request,
    const std::optional<PushNotificationDesktopApiCallFlow::QueryParameters>&
        request_as_query_parameters,
    base::OnceCallback<void(const ResponseProto&)>&& response_callback,
    ErrorCallback&& error_callback,
    const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) {
  CHECK(!has_call_started_) << "PushNotificationServerClientDesktopImpl::"
                               "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(kPushNotificationOath2Scope);

  access_token_fetcher_ =
      std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
          kPushNotificationOAuthName, identity_manager_, scopes,
          base::BindOnce(
              &PushNotificationServerClientDesktopImpl::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 PushNotificationServerClientDesktopImpl::OnAccessTokenFetched(
    RequestType request_type,
    const std::optional<std::string>& serialized_request,
    const std::optional<PushNotificationDesktopApiCallFlow::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(PushNotificationDesktopApiCallFlow::
                        PushNotificationApiCallFlowError::kAuthenticationError);
    metrics::RecordPushNotificationOAuthTokenRetrievalResult(/*success=*/false);
    return;
  }
  metrics::RecordPushNotificationOAuthTokenRetrievalResult(/*success=*/true);
  access_token_used_ = access_token_info.token;
  CHECK(access_token_used_.has_value());
  proto::NotificationsMultiLoginUpdateRequest request_proto;

  switch (request_type) {
    case RequestType::kGet:
      CHECK(request_as_query_parameters && !serialized_request);
      api_call_flow_->StartGetRequest(
          request_url_, *request_as_query_parameters, url_loader_factory_,
          access_token_used_.value(),
          base::BindOnce(
              &PushNotificationServerClientDesktopImpl::OnFlowSuccess<
                  ResponseProto>,
              weak_ptr_factory_.GetWeakPtr(), std::move(response_callback)),
          base::BindOnce(
              &PushNotificationServerClientDesktopImpl::OnApiCallFailed,
              weak_ptr_factory_.GetWeakPtr()));
      break;
    case RequestType::kPost:
      CHECK(serialized_request && !request_as_query_parameters);

      // Copy a new `NotificationsMultiLoginUpdateRequest` proto so we can add
      // the access token as the oauth token.
      request_proto.ParseFromString(serialized_request.value());
      request_proto.mutable_registrations(0)
          ->mutable_user_id()
          ->mutable_gaia_credentials()
          ->set_oauth_token(access_token_used_.value());

      api_call_flow_->StartPostRequest(
          request_url_, request_proto.SerializeAsString(), url_loader_factory_,
          access_token_used_.value(),
          base::BindOnce(
              &PushNotificationServerClientDesktopImpl::OnFlowSuccess<
                  ResponseProto>,
              weak_ptr_factory_.GetWeakPtr(), std::move(response_callback)),
          base::BindOnce(
              &PushNotificationServerClientDesktopImpl::OnApiCallFailed,
              weak_ptr_factory_.GetWeakPtr()));
      break;
    case RequestType::kPatch:
      CHECK(serialized_request && !request_as_query_parameters);
      api_call_flow_->StartPatchRequest(
          request_url_, *serialized_request, url_loader_factory_,
          access_token_used_.value(),
          base::BindOnce(
              &PushNotificationServerClientDesktopImpl::OnFlowSuccess<
                  ResponseProto>,
              weak_ptr_factory_.GetWeakPtr(), std::move(response_callback)),
          base::BindOnce(
              &PushNotificationServerClientDesktopImpl::OnApiCallFailed,
              weak_ptr_factory_.GetWeakPtr()));
      break;
  }
}

template <class ResponseProto>
void PushNotificationServerClientDesktopImpl::OnFlowSuccess(
    base::OnceCallback<void(const ResponseProto&)>&& result_callback,
    const std::string& serialized_response) {
  ResponseProto response;
  if (!response.ParseFromString(serialized_response)) {
    OnApiCallFailed(PushNotificationDesktopApiCallFlow::
                        PushNotificationApiCallFlowError::kResponseMalformed);
    return;
  }
  std::move(result_callback).Run(response);
}

void PushNotificationServerClientDesktopImpl::OnApiCallFailed(
    PushNotificationDesktopApiCallFlow::PushNotificationApiCallFlowError
        error) {
  std::move(error_callback_).Run(error);
}

}  // namespace push_notification