chromium/chrome/browser/ash/policy/networking/euicc_status_uploader.cc

// Copyright 2021 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/ash/policy/networking/euicc_status_uploader.h"

#include "ash/constants/ash_features.h"
#include "base/json/json_string_value_serializer.h"
#include "base/metrics/histogram_functions.h"
#include "base/timer/timer.h"
#include "base/values.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ash/settings/device_settings_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/network/cellular_esim_profile_handler.h"
#include "chromeos/ash/components/network/managed_cellular_pref_handler.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "components/onc/onc_constants.h"
#include "components/policy/core/common/cloud/cloud_policy_client.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"

namespace policy {

namespace {

const char kLastUploadedEuiccStatusEuiccCountKey[] = "euicc_count";
const char kLastUploadedEuiccStatusESimProfilesKey[] = "esim_profiles";
const char kLastUploadedEuiccStatusESimProfilesIccidKey[] = "iccid";
const char kLastUploadedEuiccStatusESimProfilesNetworkNameKey[] =
    "network_name";
const char kLastUploadedEuiccStatusESimProfilesSmdpActivationCodeKey[] =
    "smdp_activation_code";
const char kLastUploadedEuiccStatusESimProfilesSmdsActivationCodeKey[] =
    "smds_activation_code";

const net::BackoffEntry::Policy kBackOffPolicy = {
    // Number of initial errors (in sequence) to ignore before applying
    // exponential back-off rules.
    0,

    // Initial delay for exponential back-off in ms.
    static_cast<int>(base::Minutes(5).InMilliseconds()),

    // Factor by which the waiting time will be multiplied.
    2,

    // Fuzzing percentage. ex: 10% will spread requests randomly
    // between 90%-100% of the calculated time.
    0.5,
    // Maximum amount of time we are willing to delay our request in ms.
    static_cast<int>(base::Hours(6).InMilliseconds()),

    // Time to keep an entry from being discarded even when it
    // has no significant state, -1 to never discard.
    -1,

    // Starts with initial delay.
    true,
};

// Returns whether the device's policy data is active and provisioned.
bool IsDeviceManaged() {
  return ::ash::DeviceSettingsService::IsInitialized() &&
         ::ash::DeviceSettingsService::Get()->policy_data() &&
         ::ash::DeviceSettingsService::Get()->policy_data()->state() ==
             enterprise_management::PolicyData::ACTIVE;
}

}  // namespace

// static
const char EuiccStatusUploader::kLastUploadedEuiccStatusPref[] =
    "esim.last_uploaded_euicc_status";
const char EuiccStatusUploader::kShouldSendClearProfilesRequestPref[] =
    "esim.should_clear_profile_list";

EuiccStatusUploader::EuiccStatusUploader(CloudPolicyClient* client,
                                         PrefService* local_state)
    : EuiccStatusUploader(client,
                          local_state,
                          base::BindRepeating(&IsDeviceManaged)) {}

EuiccStatusUploader::EuiccStatusUploader(
    CloudPolicyClient* client,
    PrefService* local_state,
    IsDeviceActiveCallback is_device_active_callback)
    : client_(client),
      local_state_(local_state),
      is_device_managed_callback_(std::move(is_device_active_callback)),
      retry_entry_(&kBackOffPolicy) {
  if (!ash::NetworkHandler::IsInitialized()) {
    LOG(WARNING) << "NetworkHandler is not initialized.";
    return;
  }

  hermes_manager_observation_.Observe(ash::HermesManagerClient::Get());
  hermes_euicc_observation_.Observe(ash::HermesEuiccClient::Get());
  cloud_policy_client_observation_.Observe(client_.get());

  auto* network_handler = ash::NetworkHandler::Get();
  network_handler->managed_cellular_pref_handler()->AddObserver(this);
  managed_network_configuration_handler_ =
      network_handler->managed_network_configuration_handler();
  managed_network_configuration_handler_->AddObserver(this);
}

EuiccStatusUploader::~EuiccStatusUploader() {
  if (ash::NetworkHandler::IsInitialized()) {
    ash::NetworkHandler::Get()->managed_cellular_pref_handler()->RemoveObserver(
        this);
  }
  OnManagedNetworkConfigurationHandlerShuttingDown();
}

// static
void EuiccStatusUploader::RegisterLocalStatePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(kLastUploadedEuiccStatusPref,
                                   PrefRegistry::NO_REGISTRATION_FLAGS);
  registry->RegisterBooleanPref(kShouldSendClearProfilesRequestPref,
                                /*default_value=*/false);
}

// static
std::unique_ptr<enterprise_management::UploadEuiccInfoRequest>
EuiccStatusUploader::ConstructRequestFromStatus(const base::Value::Dict& status,
                                                bool clear_profile_list) {
  auto upload_request =
      std::make_unique<enterprise_management::UploadEuiccInfoRequest>();
  upload_request->set_euicc_count(
      status.FindInt(kLastUploadedEuiccStatusEuiccCountKey).value());

  auto* mutable_esim_profiles = upload_request->mutable_esim_profiles();
  for (const auto& esim_profile :
       *status.FindListByDottedPath(kLastUploadedEuiccStatusESimProfilesKey)) {
    const base::Value::Dict& esim_profile_dict = esim_profile.GetDict();
    enterprise_management::ESimProfileInfo esim_profile_info;
    esim_profile_info.set_iccid(*esim_profile_dict.FindString(
        kLastUploadedEuiccStatusESimProfilesIccidKey));

    const std::string* network_name = esim_profile_dict.FindString(
        kLastUploadedEuiccStatusESimProfilesNetworkNameKey);
    if (network_name && !network_name->empty()) {
      esim_profile_info.set_name(*network_name);
    }

    const std::string* smdp_activation_code = esim_profile_dict.FindString(
        kLastUploadedEuiccStatusESimProfilesSmdpActivationCodeKey);
    const std::string* smds_activation_code = esim_profile_dict.FindString(
        kLastUploadedEuiccStatusESimProfilesSmdsActivationCodeKey);

    if (smdp_activation_code && !smdp_activation_code->empty()) {
      esim_profile_info.set_smdp_address(*smdp_activation_code);
    } else if (smds_activation_code && !smds_activation_code->empty()) {
      esim_profile_info.set_smds_address(*smds_activation_code);
    } else {
      NET_LOG(ERROR) << "Failed to find an activation code when constructing "
                        "EUICC upload request";
      continue;
    }

    mutable_esim_profiles->Add(std::move(esim_profile_info));
  }
  upload_request->set_clear_profile_list(clear_profile_list);
  return upload_request;
}

void EuiccStatusUploader::OnManagedNetworkConfigurationHandlerShuttingDown() {
  if (managed_network_configuration_handler_ &&
      managed_network_configuration_handler_->HasObserver(this)) {
    managed_network_configuration_handler_->RemoveObserver(this);
  }
  managed_network_configuration_handler_ = nullptr;
}

void EuiccStatusUploader::OnRegistrationStateChanged(
    CloudPolicyClient* client) {
  MaybeUploadStatus();
}

void EuiccStatusUploader::OnPolicyFetched(CloudPolicyClient* client) {
  if (is_policy_fetched_) {
    return;
  }
  is_policy_fetched_ = true;
  MaybeUploadStatus();
}

void EuiccStatusUploader::PoliciesApplied(const std::string& userhash) {
  MaybeUploadStatus();
}

void EuiccStatusUploader::OnManagedCellularPrefChanged() {
  MaybeUploadStatus();
}

void EuiccStatusUploader::OnAvailableEuiccListChanged() {
  MaybeUploadStatus();
}

void EuiccStatusUploader::OnEuiccReset(const dbus::ObjectPath& euicc_path) {
  // Remember that we should clear the profile list in the next upload. This
  // ensures that profile list will be eventually cleared even if the immediate
  // uploads do not succeed.
  local_state_->SetBoolean(kShouldSendClearProfilesRequestPref, true);
  MaybeUploadStatus();
}

base::Value::Dict EuiccStatusUploader::GetCurrentEuiccStatus() const {
  auto status = base::Value::Dict().Set(
      kLastUploadedEuiccStatusEuiccCountKey,
      static_cast<int>(
          ash::HermesManagerClient::Get()->GetAvailableEuiccs().size()));

  base::Value::List esim_profiles;

  for (const auto& esim_profile : ash::NetworkHandler::Get()
                                      ->cellular_esim_profile_handler()
                                      ->GetESimProfiles()) {
    // Do not report non-provisioned cellular networks.
    if (esim_profile.iccid().empty()) {
      continue;
    }

    const base::Value::Dict* esim_metadata =
        ash::NetworkHandler::Get()
            ->managed_cellular_pref_handler()
            ->GetESimMetadata(esim_profile.iccid());

    // Report only managed profiles that we have metadata for.
    if (!esim_metadata) {
      continue;
    }

    base::Value::Dict esim_profile_to_add;
    esim_profile_to_add.Set(kLastUploadedEuiccStatusESimProfilesIccidKey,
                            esim_profile.iccid());

    const std::string* const smdp_activation_code =
        esim_metadata->FindString(::onc::cellular::kSMDPAddress);
    const std::string* const smds_activation_code =
        esim_metadata->FindString(::onc::cellular::kSMDSAddress);

    if (smdp_activation_code && !smdp_activation_code->empty()) {
      esim_profile_to_add.Set(
          kLastUploadedEuiccStatusESimProfilesSmdpActivationCodeKey,
          *smdp_activation_code);
    } else if (smds_activation_code && !smds_activation_code->empty()) {
      esim_profile_to_add.Set(
          kLastUploadedEuiccStatusESimProfilesSmdsActivationCodeKey,
          *smds_activation_code);
    } else {
      // Report only managed profiles that we have an activation code for.
      NET_LOG(ERROR) << "Failed to find an SM-DP+ or SM-DS activation code "
                     << "in the eSIM metadata, skipping entry";
      continue;
    }

    const std::string* network_name =
        esim_metadata->FindString(::onc::network_config::kName);
    if (network_name && !network_name->empty()) {
      esim_profile_to_add.Set(
          kLastUploadedEuiccStatusESimProfilesNetworkNameKey, *network_name);
    }

    esim_profiles.Append(std::move(esim_profile_to_add));
  }

  status.SetByDottedPath(kLastUploadedEuiccStatusESimProfilesKey,
                         std::move(esim_profiles));
  return status;
}

void EuiccStatusUploader::MaybeUploadStatus() {
  if (!client_->is_registered()) {
    VLOG(1) << "Policy client is not registered.";
    return;
  }

  if (!is_policy_fetched_) {
    VLOG(1) << "Policy not fetched yet.";
    return;
  }

  if (!is_device_managed_callback_.Run()) {
    VLOG(1) << "Device is unmanaged or deprovisioned.";
    return;
  }

  if (!managed_network_configuration_handler_) {
    LOG(WARNING) << "ManageNetworkConfigurationHandler is not initialized.";
    return;
  }

  if (ash::HermesManagerClient::Get()->GetAvailableEuiccs().empty()) {
    VLOG(1) << "No EUICC available on the device.";
    return;
  }

  const base::Value::Dict& last_uploaded_pref =
      local_state_->GetDict(kLastUploadedEuiccStatusPref);
  auto current_state = GetCurrentEuiccStatus();

  // Force send the status if reset request was received.
  if (local_state_->GetBoolean(kShouldSendClearProfilesRequestPref)) {
    UploadStatus(std::move(current_state));
    return;
  }

  if (attempted_upload_status_ == current_state) {
    // We attempted to upload this status, but failed.
    // Schedule retry.
    if (!retry_timer_) {
      retry_timer_ = std::make_unique<base::OneShotTimer>();
      retry_timer_->Start(FROM_HERE, retry_entry_.GetTimeUntilRelease(),
                          base::BindOnce(&EuiccStatusUploader::RetryUpload,
                                         weak_ptr_factory_.GetWeakPtr()));
    }
    return;
  }

  retry_timer_.reset();

  if (last_uploaded_pref != current_state) {
    UploadStatus(std::move(current_state));
  }
}

void EuiccStatusUploader::UploadStatus(base::Value::Dict status) {
  // Do not upload anything until the current upload finishes.
  if (currently_uploading_) {
    return;
  }
  currently_uploading_ = true;
  attempted_upload_status_ = std::move(status);

  const bool should_send_clear_profiles_request =
      local_state_->GetBoolean(kShouldSendClearProfilesRequestPref);

  auto upload_request = ConstructRequestFromStatus(
      attempted_upload_status_, should_send_clear_profiles_request);
  client_->UploadEuiccInfo(
      std::move(upload_request),
      base::BindOnce(&EuiccStatusUploader::OnStatusUploaded,
                     weak_ptr_factory_.GetWeakPtr(),
                     should_send_clear_profiles_request));
}

void EuiccStatusUploader::OnStatusUploaded(
    bool should_send_clear_profiles_request,
    bool success) {
  currently_uploading_ = false;
  retry_entry_.InformOfRequest(/*succeeded=*/success);
  base::UmaHistogramBoolean(
      "Network.Cellular.ESim.Policy.EuiccStatusUploadResult", success);

  if (!success) {
    LOG(ERROR) << "Failed to upload EUICC status.";
    MaybeUploadStatus();
    return;
  }

  VLOG(1) << "EUICC status successfully uploaded.";

  // Remember the last uploaded status to not upload it again.
  local_state_->SetDict(kLastUploadedEuiccStatusPref,
                        std::move(attempted_upload_status_));

  if (should_send_clear_profiles_request) {
    // Clean out the local state preference to not send `clear_profile_list` =
    // true multiple times.
    local_state_->ClearPref(kShouldSendClearProfilesRequestPref);
  }
  attempted_upload_status_.clear();

  MaybeUploadStatus();
}

void EuiccStatusUploader::RetryUpload() {
  attempted_upload_status_.clear();
  MaybeUploadStatus();
}

void EuiccStatusUploader::FireRetryTimerIfExistsForTesting() {
  if (retry_timer_) {
    retry_timer_->FireNow();
  }
}

}  // namespace policy