chromium/chrome/browser/ash/borealis/borealis_features.cc

// Copyright 2020 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/borealis/borealis_features.h"

#include <memory>
#include <string>

#include "ash/constants/ash_features.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/system/sys_info.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/borealis/borealis_hardware_checker.h"
#include "chrome/browser/ash/borealis/borealis_prefs.h"
#include "chrome/browser/ash/guest_os/infra/cached_callback.h"
#include "chrome/browser/ash/guest_os/virtual_machines/virtual_machines_util.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/policy/profile_policy_connector.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "chromeos/ash/components/install_attributes/install_attributes.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/channel.h"

using AllowStatus = borealis::BorealisFeatures::AllowStatus;

namespace borealis {

class AsyncHardwareChecker
    : public guest_os::CachedCallback<AllowStatus, bool> {
 public:
  AsyncHardwareChecker() = default;

 private:
  void Build(RealCallback callback) override {
    // Testing hardware capabilities in unit tests is kindof pointless. The
    // following check bypasses any attempt to do async checks unless we're
    // running on a real CrOS device.
    //
    // Also do this first so we don't have to mock out statistics providers and
    // other things in tests.
    if (!base::SysInfo::IsRunningOnChromeOS()) {
      std::move(callback).Run(Success(AllowStatus::kAllowed));
      return;
    }

    // If the user has opted in to running on unsupported hardware then we don't
    // need to actually check it.
    if (base::FeatureList::IsEnabled(
            ash::features::kBorealisEnableUnsupportedHardware)) {
      std::move(callback).Run(Success(AllowStatus::kAllowed));
      return;
    }

    HasSufficientHardware(base::BindOnce(
        [](RealCallback callback, bool has_sufficient_hardware) {
          // "Success" here means we successfully determined the
          // status, which we can't really fail to do because any
          // failure to determine something is treated as a
          // disallowed status.
          std::move(callback).Run(Success(
              has_sufficient_hardware ? AllowStatus::kAllowed
                                      : AllowStatus::kInsufficientHardware));
        },
        std::move(callback)));
  }
};

BorealisFeatures::BorealisFeatures(Profile* profile)
    : profile_(profile),
      async_checker_(std::make_unique<AsyncHardwareChecker>()) {
  // Issue a request for the status immediately upon creation, in case
  // it's needed later.
  IsAllowed(base::DoNothing());
}

BorealisFeatures::~BorealisFeatures() = default;

void BorealisFeatures::IsAllowed(
    base::OnceCallback<void(AllowStatus)> callback) {
  AllowStatus partial_status = PreHardwareChecks();
  if (partial_status != AllowStatus::kAllowed) {
    std::move(callback).Run(partial_status);
    return;
  }
  async_checker_->Get(base::BindOnce(&BorealisFeatures::OnHardwareChecked,
                                     weak_factory_.GetWeakPtr(),
                                     std::move(callback)));
}

AllowStatus BorealisFeatures::PreHardwareChecks() {
  // Only put failures here if the user has no means of changing them.  I.e.
  // failures here should be as set-in-stone as hardware.
  if (!base::FeatureList::IsEnabled(features::kBorealis)) {
    return AllowStatus::kFeatureDisabled;
  }

  return AllowStatus::kAllowed;
}

AllowStatus BorealisFeatures::PostHardwareChecks() {
  // Failures here should be avoidable (in some sense) without users going and
  // replacing their hardware.
  if (!virtual_machines::AreVirtualMachinesAllowedByPolicy()) {
    return AllowStatus::kVmPolicyBlocked;
  }

  if (!profile_ || !profile_->IsRegularProfile()) {
    return AllowStatus::kBlockedOnIrregularProfile;
  }

  if (!ash::ProfileHelper::IsPrimaryProfile(profile_)) {
    return AllowStatus::kBlockedOnNonPrimaryProfile;
  }

  if (profile_->IsChild()) {
    return AllowStatus::kBlockedOnChildAccount;
  }

  const PrefService::Preference* user_allowed_pref =
      profile_->GetPrefs()->FindPreference(prefs::kBorealisAllowedForUser);
  if (!user_allowed_pref || !user_allowed_pref->GetValue()->GetBool()) {
    return AllowStatus::kUserPrefBlocked;
  }

  // For managed users the preference must be explicitly set true. So we block
  // in the case where the user is managed and the pref isn't.
  //
  // TODO(b/213398438): We migrated to using `default_for_enterprise_users` in
  // crrev.com/c/4121754, which means we should remove the below code since an
  // enterprise user will always have the policy set to its default (false).
  if (!user_allowed_pref->IsManaged() &&
      profile_->GetProfilePolicyConnector()->IsManaged()) {
    return AllowStatus::kUserPrefBlocked;
  }

  if (!base::FeatureList::IsEnabled(ash::features::kBorealisPermitted)) {
    return AllowStatus::kBlockedByFlag;
  }

  return AllowStatus::kAllowed;
}

void BorealisFeatures::OnHardwareChecked(
    base::OnceCallback<void(AllowStatus)> callback,
    AsyncHardwareChecker::Result hardware_status) {
  if (!hardware_status.has_value()) {
    std::move(callback).Run(AllowStatus::kFailedToDetermine);
    return;
  }
  if (*hardware_status.value() != AllowStatus::kAllowed) {
    std::move(callback).Run(*hardware_status.value());
    return;
  }
  std::move(callback).Run(PostHardwareChecks());
}

bool BorealisFeatures::IsEnabled() {
  return profile_->GetPrefs()->GetBoolean(prefs::kBorealisInstalledOnDevice);
}

}  // namespace borealis

std::ostream& operator<<(std::ostream& os, const AllowStatus& reason) {
  switch (reason) {
    case AllowStatus::kAllowed:
      return os << "Borealis is allowed";
    case AllowStatus::kFeatureDisabled:
      return os << "Borealis has not been released on this device";
    case AllowStatus::kFailedToDetermine:
      return os << "Could not verify that Borealis was allowed. Please Retry "
                   "in a bit";
    case AllowStatus::kBlockedOnIrregularProfile:
      return os << "Borealis is only available on normal login sessions";
    case AllowStatus::kBlockedOnNonPrimaryProfile:
      return os << "Borealis is only available on the primary profile";
    case AllowStatus::kBlockedOnChildAccount:
      return os << "Borealis is not available on child accounts";
    case AllowStatus::kVmPolicyBlocked:
      return os << "Your admin has blocked borealis (virtual machines are "
                   "disabled)";
    case AllowStatus::kUserPrefBlocked:
      return os << "Your admin has blocked borealis (for your account)";
    case AllowStatus::kBlockedByFlag:
      return os << "Borealis is still being worked on. You must set the "
                   "#borealis-enabled feature flag.";
    case AllowStatus::kInsufficientHardware:
      return os << "Borealis is not supported on this hardware";
  }
}