chromium/chrome/browser/ash/policy/enrollment/auto_enrollment_controller.cc

// Copyright 2014 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/enrollment/auto_enrollment_controller.h"

#include <memory>
#include <string_view>

#include "ash/constants/ash_switches.h"
#include "base/check_is_test.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/login/oobe_configuration.h"
#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
#include "chrome/browser/ash/policy/enrollment/auto_enrollment_client.h"
#include "chrome/browser/ash/policy/enrollment/auto_enrollment_client_impl.h"
#include "chrome/browser/ash/policy/enrollment/auto_enrollment_state.h"
#include "chrome/browser/ash/policy/enrollment/auto_enrollment_type_checker.h"
#include "chrome/browser/ash/policy/enrollment/enrollment_state_fetcher.h"
#include "chrome/browser/ash/policy/enrollment/psm/construct_rlwe_id.h"
#include "chrome/browser/ash/policy/enrollment/psm/rlwe_dmserver_client_impl.h"
#include "chrome/browser/ash/policy/server_backed_state/server_backed_state_keys_broker.h"
#include "chrome/browser/ash/settings/device_settings_service.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chromeos/ash/components/dbus/cryptohome/rpc.pb.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
#include "chromeos/ash/components/dbus/device_management/install_attributes_client.h"
#include "chromeos/ash/components/dbus/system_clock/system_clock_client.h"
#include "chromeos/ash/components/dbus/system_clock/system_clock_sync_observation.h"
#include "chromeos/ash/components/install_attributes/install_attributes.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "components/policy/core/common/cloud/device_management_service.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

// This is used for logs that may not be strictly necessary but are of great use
// because they will log whether determinations are needed or not, along with
// some context. The information used to be logged using VLOG(1), and therefore
// was not available in customer logs. Because the only other logs have some
// ambiguity (e.g. there will not be a log if the device decides it does not
// need to make a determination), troubleshooting is difficult. If this changes,
// this can be made VLOG(1) again.
//
// We use LOG(WARNING) to guarantee that the messages will be into feedback
// reports.
#define LOG_DETERMINATION() LOG(WARNING)

namespace policy {
namespace {

const int kMaxRequestStateKeysTries = 10;

// Maximum time to wait for the auto-enrollment check to reach a decision.
// Note that this encompasses all steps `AutoEnrollmentController` performs in
// order to determine if the device should be auto-enrolled.
// If `kSafeguardTimeout` after `Start()` has been called,
// `AutoEnrollmentController::state()` is still AutoEnrollmentState::kPending,
// the AutoEnrollmentController will switch to
// `AutoEnrollmentResult::kNoEnrollment` or
// `AutoEnrollmentSafeguardTimeoutError` (see
// `AutoEnrollmentController::Timeout`). Note that this timeout should not be
// too short, because one of the steps `AutoEnrollmentController` performs -
// downloading identifier hash buckets - can be non-negligible, especially on 2G
// connections.
constexpr base::TimeDelta kSafeguardTimeout = base::Seconds(90);

// Maximum time to wait for time sync before forcing a decision on whether
// Initial Enrollment should be performed. This corresponds to at least seven
// TCP retransmissions attempts to the remote server used to update the system
// clock.
constexpr base::TimeDelta kSystemClockSyncWaitTimeout = base::Seconds(45);

// A callback that will be invoked when the system clock has been synchronized,
// or if system clock synchronization has failed.
using SystemClockSyncCallback = base::OnceCallback<void(
    AutoEnrollmentController::SystemClockSyncState system_clock_sync_state)>;

// Returns the int value of the `switch_name` argument, clamped to the [0, 62]
// interval. Returns 0 if the argument doesn't exist or isn't an int value.
int GetSanitizedArg(const std::string& switch_name) {
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  if (!command_line->HasSwitch(switch_name)) {
    return 0;
  }
  std::string value = command_line->GetSwitchValueASCII(switch_name);
  int int_value;
  if (!base::StringToInt(value, &int_value)) {
    LOG(ERROR) << "Switch \"" << switch_name << "\" is not a valid int. "
               << "Defaulting to 0.";
    return 0;
  }
  if (int_value < 0) {
    LOG(ERROR) << "Switch \"" << switch_name << "\" can't be negative. "
               << "Using 0";
    return 0;
  }
  if (int_value > AutoEnrollmentClient::kMaximumPower) {
    LOG(ERROR) << "Switch \"" << switch_name << "\" can't be greater than "
               << AutoEnrollmentClient::kMaximumPower << ". Using "
               << AutoEnrollmentClient::kMaximumPower << ".";
    return AutoEnrollmentClient::kMaximumPower;
  }
  return int_value;
}

bool IsSystemClockSynchronized(
    AutoEnrollmentController::SystemClockSyncState state) {
  switch (state) {
    case AutoEnrollmentController::SystemClockSyncState::kSynchronized:
    case AutoEnrollmentController::SystemClockSyncState::kSyncFailed:
      return true;
    case AutoEnrollmentController::SystemClockSyncState::kCanWaitForSync:
    case AutoEnrollmentController::SystemClockSyncState::kWaitingForSync:
      return false;
  }
}

enum class AutoEnrollmentControllerTimeoutReport {
  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused.
  kTimeoutCancelled = 0,
  kTimeoutFRE = 1,
  kTimeout = 2,
  kTimeoutUnified = 3,
  kMaxValue = kTimeoutUnified
};

void ReportTimeoutUMA(AutoEnrollmentControllerTimeoutReport report) {
  base::UmaHistogramEnumeration("Enterprise.AutoEnrollmentControllerTimeout",
                                report);
}

bool IsFinalAutoEnrollmentState(AutoEnrollmentState state) {
  return state.has_value();
}

}  // namespace

EnrollmentFwmpHelper::EnrollmentFwmpHelper(
    ash::InstallAttributesClient* install_attributes_client)
    : install_attributes_client_(install_attributes_client) {}

EnrollmentFwmpHelper::~EnrollmentFwmpHelper() = default;

void EnrollmentFwmpHelper::DetermineDevDisableBoot(
    ResultCallback result_callback) {
  // D-Bus services may not be available yet, so we call
  // WaitForServiceToBeAvailable. See https://crbug.com/841627.
  install_attributes_client_->WaitForServiceToBeAvailable(base::BindOnce(
      &EnrollmentFwmpHelper::RequestFirmwareManagementParameters,
      weak_ptr_factory_.GetWeakPtr(), std::move(result_callback)));
}

void EnrollmentFwmpHelper::RequestFirmwareManagementParameters(
    ResultCallback result_callback,
    bool service_is_ready) {
  if (!service_is_ready) {
    LOG(ERROR) << "Failed waiting for cryptohome D-Bus service availability.";
    return std::move(result_callback).Run(false);
  }

  device_management::GetFirmwareManagementParametersRequest request;
  install_attributes_client_->GetFirmwareManagementParameters(
      request,
      base::BindOnce(
          &EnrollmentFwmpHelper::OnGetFirmwareManagementParametersReceived,
          weak_ptr_factory_.GetWeakPtr(), std::move(result_callback)));
}

void EnrollmentFwmpHelper::OnGetFirmwareManagementParametersReceived(
    ResultCallback result_callback,
    std::optional<device_management::GetFirmwareManagementParametersReply>
        reply) {
  if (!reply.has_value() || reply->error() !=
                                device_management::DeviceManagementErrorCode::
                                    DEVICE_MANAGEMENT_ERROR_NOT_SET) {
    LOG(ERROR) << "Failed to retrieve firmware management parameters.";
    return std::move(result_callback).Run(false);
  }

  const bool dev_disable_boot =
      (reply->fwmp().flags() & cryptohome::DEVELOPER_DISABLE_BOOT);

  std::move(result_callback).Run(dev_disable_boot);
}

AutoEnrollmentController::AutoEnrollmentController(
    scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory)
    : AutoEnrollmentController(
          ash::DeviceSettingsService::Get(),
          g_browser_process->platform_part()
              ->browser_policy_connector_ash()
              ->device_management_service(),
          g_browser_process->platform_part()
              ->browser_policy_connector_ash()
              ->GetStateKeysBroker(),
          ash::NetworkHandler::Get()->network_state_handler(),
          std::make_unique<AutoEnrollmentClientImpl::FactoryImpl>(),
          base::BindRepeating(&policy::psm::RlweDmserverClientImpl::Create),
          base::BindRepeating(EnrollmentStateFetcher::Create),
          shared_url_loader_factory) {}

AutoEnrollmentController::AutoEnrollmentController(
    ash::DeviceSettingsService* device_settings_service,
    DeviceManagementService* device_management_service,
    ServerBackedStateKeysBroker* state_keys_broker,
    ash::NetworkStateHandler* network_state_handler,
    std::unique_ptr<AutoEnrollmentClient::Factory>
        auto_enrollment_client_factory,
    RlweClientFactory psm_rlwe_client_factory,
    EnrollmentStateFetcher::Factory enrollment_state_fetcher_factory,
    scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory)
    : device_settings_service_(device_settings_service),
      device_management_service_(device_management_service),
      state_keys_broker_(state_keys_broker),
      enrollment_fwmp_helper_(std::make_unique<EnrollmentFwmpHelper>(
          ash::InstallAttributesClient::Get())),
      auto_enrollment_client_factory_(
          std::move(auto_enrollment_client_factory)),
      psm_rlwe_client_factory_(std::move(psm_rlwe_client_factory)),
      enrollment_state_fetcher_factory_(
          std::move(enrollment_state_fetcher_factory)),
      shared_url_loader_factory_(shared_url_loader_factory),
      network_state_handler_(network_state_handler) {}

AutoEnrollmentController::~AutoEnrollmentController() = default;

void AutoEnrollmentController::Start() {
  LOG(WARNING) << "Starting auto-enrollment controller.";

  if (state_.has_value() && IsFinalAutoEnrollmentState(state_.value())) {
    return;
  }

  if (!network_state_observation_.IsObserving()) {
    // The controller could have already subscribed on the start and now we're
    // restarting after an error.
    network_state_observation_.Observe(network_state_handler_);
  }

  if (!AutoEnrollmentTypeChecker::Initialized()) {
    if (!auto_enrollment_check_type_init_started_) {
      auto_enrollment_check_type_init_started_ = true;
      AutoEnrollmentTypeChecker::Initialize(
          shared_url_loader_factory_,
          base::BindOnce(&AutoEnrollmentController::Start,
                         weak_ptr_factory_.GetWeakPtr()));
    }
    return;
  }

  if (IsInProgress()) {
    return;
  }

  // Arm the belts-and-suspenders timer to avoid hangs.
  safeguard_timer_.Start(FROM_HERE, kSafeguardTimeout,
                         base::BindOnce(&AutoEnrollmentController::Timeout,
                                        weak_ptr_factory_.GetWeakPtr()));

  if (AutoEnrollmentTypeChecker::IsUnifiedStateDeterminationEnabled()) {
    // Emulate required FRE to prevent users from skipping enrollment.
    auto_enrollment_check_type_ = AutoEnrollmentTypeChecker::CheckType::
        kForcedReEnrollmentExplicitlyRequired;

    device_management_service_->ScheduleInitialization(0);
    enrollment_state_fetcher_ = enrollment_state_fetcher_factory_.Run(
        base::BindRepeating(&AutoEnrollmentController::UpdateState,
                            weak_ptr_factory_.GetWeakPtr()),
        g_browser_process->local_state(), psm_rlwe_client_factory_,
        device_management_service_, shared_url_loader_factory_,
        ash::SystemClockClient::Get(), state_keys_broker_,
        device_settings_service_, ash::OobeConfiguration::Get());

    enrollment_state_fetcher_->Start();
    return;
  }

  request_state_keys_tries_ = 0;

  // The system clock sync state is not known yet, and this
  // `AutoEnrollmentController` could wait for it if requested.
  system_clock_sync_state_ = SystemClockSyncState::kCanWaitForSync;

  enrollment_fwmp_helper_->DetermineDevDisableBoot(
      base::BindOnce(&AutoEnrollmentController::OnDevDisableBootDetermined,
                     weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::OnDevDisableBootDetermined(
    bool dev_disable_boot) {
  dev_disable_boot_ = dev_disable_boot;

  StartWithSystemClockSyncState();
}

void AutoEnrollmentController::StartWithSystemClockSyncState() {
  auto_enrollment_check_type_ =
      AutoEnrollmentTypeChecker::DetermineAutoEnrollmentCheckType(
          IsSystemClockSynchronized(system_clock_sync_state_),
          ash::system::StatisticsProvider::GetInstance(), dev_disable_boot_);
  if (auto_enrollment_check_type_ ==
      AutoEnrollmentTypeChecker::CheckType::kNone) {
    UpdateState(AutoEnrollmentResult::kNoEnrollment);
    return;
  }
  // If waiting for system clock synchronization has been triggered, wait until
  // it finishes (this function will be called again when a result is
  // available).
  if (system_clock_sync_state_ == SystemClockSyncState::kWaitingForSync) {
    return;
  }

  if (auto_enrollment_check_type_ == AutoEnrollmentTypeChecker::CheckType::
                                         kUnknownDueToMissingSystemClockSync) {
    DCHECK_EQ(system_clock_sync_state_, SystemClockSyncState::kCanWaitForSync);
    system_clock_sync_state_ = SystemClockSyncState::kWaitingForSync;

    LOG(WARNING) << "Waiting for clock sync";
    // Use `client_start_weak_factory_` so the callback is not invoked if
    // `Timeout` has been called in the meantime (after `kSafeguardTimeout`).
    system_clock_sync_observation_ =
        ash::SystemClockSyncObservation::WaitForSystemClockSync(
            ash::SystemClockClient::Get(), kSystemClockSyncWaitTimeout,
            base::BindOnce(&AutoEnrollmentController::OnSystemClockSyncResult,
                           client_start_weak_factory_.GetWeakPtr()));
    return;
  }

  LOG(WARNING) << "Get ownership status to check if it's enrollment recovery";
  device_settings_service_->GetOwnershipStatusAsync(
      base::BindOnce(&AutoEnrollmentController::OnOwnershipStatusCheckDone,
                     client_start_weak_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::Retry() {
  if (client_) {
    client_->Retry();
  } else {
    Start();
  }
}

base::CallbackListSubscription
AutoEnrollmentController::RegisterProgressCallback(
    const ProgressCallbackList::CallbackType& callback) {
  return progress_callbacks_.Add(callback);
}

void AutoEnrollmentController::PortalStateChanged(
    const ash::NetworkState* /*default_network*/,
    const ash::NetworkState::PortalState portal_state) {
  // It is safe to retry regardless of the current state: if the check is idle
  // or failed, we will restart the check process. If the check is in progress,
  // the retry call will be ignored.
  if (portal_state == ash::NetworkState::PortalState::kOnline) {
    Retry();
  }
}

void AutoEnrollmentController::OnShuttingDown() {
  network_state_observation_.Reset();
}

void AutoEnrollmentController::SetRlweClientFactoryForTesting(
    RlweClientFactory test_factory) {
  CHECK_IS_TEST();
  psm_rlwe_client_factory_ = std::move(test_factory);
}

void AutoEnrollmentController::SetAutoEnrollmentClientFactoryForTesting(
    std::unique_ptr<AutoEnrollmentClient::Factory>
        auto_enrollment_client_factory) {
  CHECK_IS_TEST();
  auto_enrollment_client_factory_ = std::move(auto_enrollment_client_factory);
}

void AutoEnrollmentController::OnOwnershipStatusCheckDone(
    ash::DeviceSettingsService::OwnershipStatus status) {
  switch (status) {
    case ash::DeviceSettingsService::OwnershipStatus::kOwnershipNone:
      switch (auto_enrollment_check_type_) {
        case AutoEnrollmentTypeChecker::CheckType::
            kForcedReEnrollmentExplicitlyRequired:
        case AutoEnrollmentTypeChecker::CheckType::
            kForcedReEnrollmentImplicitlyRequired:
          ++request_state_keys_tries_;
          // For FRE, request state keys first.
          LOG(WARNING) << "Requesting state keys. Attempt "
                       << request_state_keys_tries_ << ".";
          state_keys_broker_->RequestStateKeys(
              base::BindOnce(&AutoEnrollmentController::StartClientForFRE,
                             client_start_weak_factory_.GetWeakPtr()));
          break;
        case AutoEnrollmentTypeChecker::CheckType::kInitialStateDetermination:
          LOG(WARNING) << "Start client for initial state determination.";
          StartClientForInitialEnrollment();
          break;
        case AutoEnrollmentTypeChecker::CheckType::
            kUnknownDueToMissingSystemClockSync:
        case AutoEnrollmentTypeChecker::CheckType::kNone:
          // The ownership check is only triggered if
          // `auto_enrollment_check_type_` indicates that an auto-enrollment
          // check should be done.
          NOTREACHED_IN_MIGRATION();
          break;
      }
      return;
    case ash::DeviceSettingsService::OwnershipStatus::kOwnershipTaken:
      LOG(WARNING) << "Device already owned, skipping auto-enrollment check.";
      UpdateState(AutoEnrollmentResult::kNoEnrollment);
      return;
    case ash::DeviceSettingsService::OwnershipStatus::kOwnershipUnknown:
      LOG(ERROR) << "Ownership unknown, skipping auto-enrollment check.";
      UpdateState(AutoEnrollmentResult::kNoEnrollment);
      return;
  }
}

void AutoEnrollmentController::StartClientForFRE(
    const std::vector<std::string>& state_keys) {
  if (state_keys.empty()) {
    LOG(ERROR) << "No state keys available.";
    if (auto_enrollment_check_type_ ==
        AutoEnrollmentTypeChecker::CheckType::
            kForcedReEnrollmentExplicitlyRequired) {
      if (request_state_keys_tries_ >= kMaxRequestStateKeysTries) {
        if (safeguard_timer_.IsRunning()) {
          safeguard_timer_.Stop();
        }
        Timeout();
        return;
      }
      ++request_state_keys_tries_;
      // Retry to fetch the state keys. For devices where FRE is required to be
      // checked, we can't proceed with empty state keys.
      state_keys_broker_->RequestStateKeys(
          base::BindOnce(&AutoEnrollmentController::StartClientForFRE,
                         client_start_weak_factory_.GetWeakPtr()));
    } else {
      UpdateState(AutoEnrollmentResult::kNoEnrollment);
    }
    return;
  }

  int power_initial =
      GetSanitizedArg(ash::switches::kEnterpriseEnrollmentInitialModulus);
  int power_limit =
      GetSanitizedArg(ash::switches::kEnterpriseEnrollmentModulusLimit);
  if (power_initial > power_limit) {
    LOG(ERROR) << "Initial auto-enrollment modulus is larger than the limit, "
                  "clamping to the limit.";
    power_initial = power_limit;
  }

  device_management_service_->ScheduleInitialization(0);

  client_ = auto_enrollment_client_factory_->CreateForFRE(
      base::BindRepeating(&AutoEnrollmentController::UpdateState,
                          weak_ptr_factory_.GetWeakPtr()),
      device_management_service_, g_browser_process->local_state(),
      shared_url_loader_factory_, state_keys.front(), power_initial,
      power_limit);

  LOG(WARNING) << "Starting auto-enrollment client for FRE.";
  client_->Start();
}

void AutoEnrollmentController::OnSystemClockSyncResult(
    bool system_clock_synchronized) {
  system_clock_sync_state_ = system_clock_synchronized
                                 ? SystemClockSyncState::kSynchronized
                                 : SystemClockSyncState::kSyncFailed;
  LOG(WARNING) << "System clock "
               << (system_clock_synchronized ? "synchronized"
                                             : "failed to synchronize");
  // Only call StartWithSystemClockSyncState() to determine the auto-enrollment
  // type if the system clock could synchronize successfully. Otherwise, return
  // an error to show to not to proceed with the auto-enrollment checks until
  // AutoEnrollmentController::Start() is called again by a network state
  // change or network selection.
  if (system_clock_sync_state_ == SystemClockSyncState::kSynchronized) {
    StartWithSystemClockSyncState();
  } else {
    UpdateState(base::unexpected(AutoEnrollmentSystemClockSyncError{}));
  }
}

void AutoEnrollmentController::StartClientForInitialEnrollment() {
  device_management_service_->ScheduleInitialization(0);

  ash::system::StatisticsProvider* provider =
      ash::system::StatisticsProvider::GetInstance();
  const std::optional<std::string_view> serial_number =
      provider->GetMachineID();
  const std::optional<std::string_view> rlz_brand_code =
      provider->GetMachineStatistic(ash::system::kRlzBrandCodeKey);
  // The Initial State Determination should not be started if the serial number
  // or brand code are missing. This is ensured in
  // `GetInitialStateDeterminationRequirement`.
  CHECK(serial_number);
  CHECK(!serial_number->empty());
  CHECK(rlz_brand_code);
  CHECK(!rlz_brand_code->empty());

  const auto plaintext_id = psm::ConstructRlweId();
  client_ = auto_enrollment_client_factory_->CreateForInitialEnrollment(
      base::BindRepeating(&AutoEnrollmentController::UpdateState,
                          weak_ptr_factory_.GetWeakPtr()),
      device_management_service_, g_browser_process->local_state(),
      shared_url_loader_factory_, std::string(serial_number.value()),
      std::string(rlz_brand_code.value()),
      std::make_unique<psm::RlweDmserverClientImpl>(
          device_management_service_, shared_url_loader_factory_, plaintext_id,
          psm_rlwe_client_factory_),
      ash::OobeConfiguration::Get());

  LOG(WARNING) << "Starting auto-enrollment client for Initial Enrollment.";
  client_->Start();
}

void AutoEnrollmentController::UpdateState(AutoEnrollmentState new_state) {
  LOG(WARNING) << "New auto-enrollment state: "
               << AutoEnrollmentStateToString(new_state);
  state_ = new_state;

  if (IsFinalAutoEnrollmentState(state_.value())) {
    network_state_observation_.Reset();
  }

  // Stop the safeguard timer once a result comes in.
  safeguard_timer_.Stop();
  // Reset enrollment state fetcher to allow restarting.
  enrollment_state_fetcher_.reset();
  ReportTimeoutUMA(AutoEnrollmentControllerTimeoutReport::kTimeoutCancelled);

  // Device disabling mode is relying on device state stored in install
  // attributes. In case that file is corrupted, this should prevent device
  // re-enabling.
  if (state_ == AutoEnrollmentResult::kDisabled) {
    DeviceMode device_mode = ash::InstallAttributes::Get()->GetMode();
    if (device_mode == DeviceMode::DEVICE_MODE_PENDING ||
        device_mode == DeviceMode::DEVICE_MODE_NOT_SET) {
      device_settings_service_->SetDeviceMode(
          DeviceMode::DEVICE_MODE_ENTERPRISE);
    }
  }

  if (state_ == AutoEnrollmentResult::kNoEnrollment ||
      state_ == AutoEnrollmentResult::kSuggestedEnrollment) {
    StartCleanupForcedReEnrollment();
  } else {
    progress_callbacks_.Notify(state_.value());
  }
}

void AutoEnrollmentController::StartCleanupForcedReEnrollment() {
  // D-Bus services may not be available yet, so we call
  // WaitForServiceToBeAvailable. See https://crbug.com/841627.
  ash::InstallAttributesClient::Get()->WaitForServiceToBeAvailable(
      base::BindOnce(
          &AutoEnrollmentController::StartRemoveFirmwareManagementParameters,
          weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::StartRemoveFirmwareManagementParameters(
    bool service_is_ready) {
  DCHECK(state_ == AutoEnrollmentResult::kNoEnrollment ||
         state_ == AutoEnrollmentResult::kSuggestedEnrollment);
  if (!service_is_ready) {
    LOG(ERROR) << "Failed waiting for cryptohome D-Bus service availability.";
    progress_callbacks_.Notify(state_.value());
    return;
  }

  device_management::RemoveFirmwareManagementParametersRequest request;
  ash::InstallAttributesClient::Get()->RemoveFirmwareManagementParameters(
      request,
      base::BindOnce(
          &AutoEnrollmentController::OnFirmwareManagementParametersRemoved,
          weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::OnFirmwareManagementParametersRemoved(
    std::optional<device_management::RemoveFirmwareManagementParametersReply>
        reply) {
  if (!reply.has_value() || reply->error() !=
                                device_management::DeviceManagementErrorCode::
                                    DEVICE_MANAGEMENT_ERROR_NOT_SET) {
    LOG(ERROR) << "Failed to remove firmware management parameters.";
  }

  // D-Bus services may not be available yet, so we call
  // WaitForServiceToBeAvailable. See https://crbug.com/841627.
  ash::SessionManagerClient::Get()->WaitForServiceToBeAvailable(
      base::BindOnce(&AutoEnrollmentController::StartClearForcedReEnrollmentVpd,
                     weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::StartClearForcedReEnrollmentVpd(
    bool service_is_ready) {
  DCHECK(state_ == AutoEnrollmentResult::kNoEnrollment ||
         state_ == AutoEnrollmentResult::kSuggestedEnrollment);
  if (!service_is_ready) {
    LOG(ERROR)
        << "Failed waiting for session_manager D-Bus service availability.";
    progress_callbacks_.Notify(state_.value());
    return;
  }

  ash::SessionManagerClient::Get()->ClearForcedReEnrollmentVpd(
      base::BindOnce(&AutoEnrollmentController::OnForcedReEnrollmentVpdCleared,
                     weak_ptr_factory_.GetWeakPtr()));
}

void AutoEnrollmentController::OnForcedReEnrollmentVpdCleared(bool reply) {
  if (!reply) {
    LOG(ERROR) << "Failed to clear forced re-enrollment flags in RW VPD.";
  }

  progress_callbacks_.Notify(state_.value());
}

void AutoEnrollmentController::Timeout() {
  if (AutoEnrollmentTypeChecker::IsUnifiedStateDeterminationEnabled()) {
    // This can actually happen in some cases, for example when state key
    // generation is waiting for time sync or the server just doesn't reply and
    // keeps the connection open.
    LOG(ERROR) << "EnrollmentStateFetcher didn't complete within time limit.";
    UpdateState(base::unexpected(AutoEnrollmentSafeguardTimeoutError{}));
    ReportTimeoutUMA(AutoEnrollmentControllerTimeoutReport::kTimeoutUnified);
    return;
  }

  // When tightening the FRE flows, as a cautionary measure (to prevent
  // interference with consumer devices) timeout was chosen to only enforce FRE
  // for EXPLICITLY_REQUIRED.
  // TODO(igorcov): Investigate the remaining causes of hitting timeout and
  // potentially either remove the timeout altogether or enforce FRE in the
  // REQUIRED case as well.
  if (client_start_weak_factory_.HasWeakPtrs() &&
      auto_enrollment_check_type_ !=
          AutoEnrollmentTypeChecker::CheckType::
              kForcedReEnrollmentExplicitlyRequired) {
    // If the callbacks to check ownership status or state keys are still
    // pending, there's a bug in the code running on the device. No use in
    // retrying anything, need to fix that bug.
    LOG(ERROR) << "Failed to start auto-enrollment check, fix the code!";
    UpdateState(AutoEnrollmentResult::kNoEnrollment);
    ReportTimeoutUMA(AutoEnrollmentControllerTimeoutReport::kTimeout);
  } else {
    // This can actually happen in some cases, for example when state key
    // generation is waiting for time sync or the server just doesn't reply and
    // keeps the connection open.
    LOG(ERROR) << "AutoEnrollmentClient didn't complete within time limit.";
    UpdateState(base::unexpected(AutoEnrollmentSafeguardTimeoutError{}));
    ReportTimeoutUMA(AutoEnrollmentControllerTimeoutReport::kTimeoutFRE);
  }

  client_.reset();

  // Make sure to nuke pending `client_` start sequences.
  client_start_weak_factory_.InvalidateWeakPtrs();
}

bool AutoEnrollmentController::IsInProgress() const {
  if (AutoEnrollmentTypeChecker::IsUnifiedStateDeterminationEnabled()) {
    if (enrollment_state_fetcher_) {
      // If a fetcher has already been created, bail out.
      LOG(ERROR) << "Enrollment state fetcher is already running.";
      return true;
    }

    return false;
  }

  // If a client is being created or already existing, bail out.
  if (client_start_weak_factory_.HasWeakPtrs() || client_) {
    LOG(ERROR) << "Enrollment state client is already running.";
    return true;
  }

  // The timer runs from `Start()` where controller starts determining state,
  // till `UpdateState()` where the controller receives a state or an error.
  // Hence it can be used to decide whether the controller is running or not.
  // If any of steps between `Start()` and `UpdateState()` are excluded from
  // the timing, or the timer is extended to some other steps, the check will
  // become wrong.
  if (safeguard_timer_.IsRunning()) {
    LOG(ERROR) << "State determination is already running.";
    return true;
  }

  return false;
}

void AutoEnrollmentController::SetEnrollmentStateFetcherFactoryForTesting(
    EnrollmentStateFetcher::Factory enrollment_state_fetcher_factory) {
  CHECK_IS_TEST();
  if (enrollment_state_fetcher_factory) {
    enrollment_state_fetcher_factory_ = enrollment_state_fetcher_factory;
  } else {
    enrollment_state_fetcher_factory_ =
        base::BindRepeating(EnrollmentStateFetcher::Create);
  }
}

}  // namespace policy