chromium/chrome/browser/ash/crosapi/browser_util.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/crosapi/browser_util.h"

#include <string>
#include <string_view>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "base/auto_reset.h"
#include "base/check_is_test.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/flat_map.h"
#include "base/debug/dump_without_crashing.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/system/sys_info.h"
#include "base/values.h"
#include "base/version.h"
#include "chrome/browser/browser_process.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chromeos/ash/components/channel/channel_info.h"
#include "chromeos/ash/components/standalone_browser/browser_support.h"
#include "chromeos/ash/components/standalone_browser/lacros_availability.h"
#include "chromeos/ash/components/standalone_browser/migrator_util.h"
#include "chromeos/ash/components/standalone_browser/standalone_browser_features.h"
#include "chromeos/crosapi/cpp/crosapi_constants.h"
#include "chromeos/crosapi/mojom/crosapi.mojom.h"
#include "components/component_updater/component_updater_service.h"
#include "components/exo/shell_surface_util.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/user_manager/known_user.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "components/version_info/channel.h"
#include "components/version_info/version_info.h"
#include "google_apis/gaia/gaia_auth_util.h"

using ash::standalone_browser::IsGoogleInternal;
using ash::standalone_browser::LacrosAvailability;
using user_manager::User;
using user_manager::UserManager;
using version_info::Channel;

namespace crosapi::browser_util {

BASE_FEATURE(kLacrosLaunchAtLoginScreen,
             "LacrosLaunchAtLoginScreen",
             base::FEATURE_DISABLED_BY_DEFAULT);

namespace {

// At session start the value for LacrosAvailability logic is applied and the
// result is stored in this variable which is used after that as a cache.
std::optional<LacrosAvailability> g_lacros_availability_cache;

// At session start the value for LacrosDataBackwardMigrationMode logic is
// applied and the result is stored in this variable which is used after that as
// a cache.
std::optional<LacrosDataBackwardMigrationMode>
    g_lacros_data_backward_migration_mode;

// The rootfs lacros-chrome metadata keys.
constexpr char kLacrosMetadataContentKey[] = "content";
constexpr char kLacrosMetadataVersionKey[] = "version";

// The conversion map for LacrosDataBackwardMigrationMode policy data. The
// values must match the ones from LacrosDataBackwardMigrationMode.yaml.
constexpr auto kLacrosDataBackwardMigrationModeMap =
    base::MakeFixedFlatMap<std::string_view, LacrosDataBackwardMigrationMode>({
        {kLacrosDataBackwardMigrationModePolicyNone,
         LacrosDataBackwardMigrationMode::kNone},
        {kLacrosDataBackwardMigrationModePolicyKeepNone,
         LacrosDataBackwardMigrationMode::kKeepNone},
        {kLacrosDataBackwardMigrationModePolicyKeepSafeData,
         LacrosDataBackwardMigrationMode::kKeepSafeData},
        {kLacrosDataBackwardMigrationModePolicyKeepAll,
         LacrosDataBackwardMigrationMode::kKeepAll},
    });

// Returns primary user's User instance.
const user_manager::User* GetPrimaryUser() {
  // TODO(crbug.com/40753373): TaskManagerImplTest is not ready to run with
  // Lacros enabled.
  // UserManager is not initialized for unit tests by default, unless a fake
  // user manager is constructed.
  if (!UserManager::IsInitialized()) {
    return nullptr;
  }

  // GetPrimaryUser works only after user session is started.
  // May return nullptr, if this is called beforehand.
  return UserManager::Get()->GetPrimaryUser();
}

// Returns the lacros integration suggested by the policy lacros-availability.
// There are several reasons why we might choose to ignore the
// lacros-availability policy.
// 1. The user has set a command line or chrome://flag for
//    kLacrosAvailabilityIgnore.
// 2. The user is a Googler and they are not opted into the
//    kLacrosGooglePolicyRollout trial and they did not have the
//    kLacrosDisallowed policy.
LacrosAvailability GetCachedLacrosAvailability() {
  // TODO(crbug.com/40210811): add DCHECK for production use to avoid the
  // same inconsistency for the future.
  if (g_lacros_availability_cache.has_value())
    return g_lacros_availability_cache.value();
  // It could happen in some browser tests that value is not cached. Return
  // default in that case.
  return LacrosAvailability::kUserChoice;
}

// Returns appropriate LacrosAvailability.
std::optional<LacrosAvailability> GetLacrosAvailability(
    const user_manager::User* user,
    ash::standalone_browser::migrator_util::PolicyInitState policy_init_state) {
  auto* user_manager = user_manager::UserManager::Get();
  auto* primary_user = user_manager->GetPrimaryUser();

  switch (policy_init_state) {
    case ash::standalone_browser::migrator_util::PolicyInitState::kBeforeInit: {
      // If the value is needed before policy initialization, actually,
      // this should be the case where ash process was restarted, and so
      // the calculated value in the previous session should be carried
      // via command line flag.
      // See also LacrosAvailabilityPolicyObserver how it will be propergated.

      // Check whether given `user` is the one for kLoginUser.
      CHECK(!primary_user);
      const user_manager::CryptohomeId cryptohome_id(
          base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
              ash::switches::kLoginUser));
      user_manager::KnownUser known_user(user_manager->GetLocalState());
      const AccountId login_account_id(
          known_user.GetAccountIdByCryptohomeId(cryptohome_id));
      if (user->GetAccountId() != login_account_id) {
        // TODO(b/40286020): Record log once the number of this call is
        // reduced.
        return std::nullopt;
      }

      return ash::standalone_browser::
          DetermineLacrosAvailabilityFromPolicyValue(
              user,
              base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
                  ash::standalone_browser::kLacrosAvailabilityPolicySwitch));
    }
    case ash::standalone_browser::migrator_util::PolicyInitState::kAfterInit: {
      // If policy initialization is done, the calculated value should be
      // cached.
      CHECK(primary_user);
      if (primary_user != user) {
        // TODO(b/40286020): Record log once the number of this call is
        // reduced.
        return std::nullopt;
      }
      return GetCachedLacrosAvailability();
    }
  }
}

}  // namespace

const char kLaunchOnLoginPref[] = "lacros.launch_on_login";
const char kProfileDataBackwardMigrationCompletedForUserPref[] =
    "lacros.profile_data_backward_migration_completed_for_user";
// This pref is to record whether the user clicks "Go to files" button
// on error page of the data migration.
const char kGotoFilesPref[] = "lacros.goto_files";
const char kProfileMigrationCompletionTimeForUserPref[] =
    "lacros.profile_migration_completion_time_for_user";

void RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(kLaunchOnLoginPref, /*default_value=*/false);
}

void RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(
      kProfileDataBackwardMigrationCompletedForUserPref);
  registry->RegisterListPref(kGotoFilesPref);
  registry->RegisterDictionaryPref(kProfileMigrationCompletionTimeForUserPref);
}

base::FilePath GetUserDataDir() {
  if (base::SysInfo::IsRunningOnChromeOS()) {
    // NOTE: On device this function is privacy/security sensitive. The
    // directory must be inside the encrypted user partition.
    return base::FilePath(crosapi::kLacrosUserDataPath);
  }
  // For developers on Linux desktop, put the directory under the developer's
  // specified --user-data-dir.
  base::FilePath base_path;
  base::PathService::Get(chrome::DIR_USER_DATA, &base_path);
  return base_path.Append("lacros");
}

bool IsLacrosAllowedToBeEnabled() {
  if (!ash::standalone_browser::BrowserSupport::IsInitializedForPrimaryUser()) {
    // This function must be called only after user session starts.
    base::debug::DumpWithoutCrashing();
    // Returning false for compatibility.
    // TODO(crbug.com/40286020): replace this logic by CHECK/DCHECK.
    return false;
  }
  return ash::standalone_browser::BrowserSupport::GetForPrimaryUser()
      ->IsAllowed();
}

bool IsLacrosEnabled() {
  return ash::standalone_browser::BrowserSupport::IsEnabledInternal(
      GetPrimaryUser(), GetCachedLacrosAvailability(),
      /*check_migration_status=*/true);
}

bool IsLacrosEnabledForMigration(
    const User* user,
    ash::standalone_browser::migrator_util::PolicyInitState policy_init_state) {
  std::optional<LacrosAvailability> lacros_availability =
      GetLacrosAvailability(user, policy_init_state);
  if (!lacros_availability.has_value()) {
    return false;
  }
  return ash::standalone_browser::BrowserSupport::IsEnabledInternal(
      user, *lacros_availability, /*check_migration_status=*/false);
}

bool IsProfileMigrationEnabled(
    const user_manager::User* user,
    ash::standalone_browser::migrator_util::PolicyInitState policy_init_state) {
  return !base::FeatureList::IsEnabled(ash::standalone_browser::features::
                                           kLacrosProfileMigrationForceOff) &&
         IsLacrosEnabledForMigration(user, policy_init_state);
}

bool IsProfileMigrationAvailable() {
  auto* user_manager = UserManager::Get();
  auto* primary_user = user_manager->GetPrimaryUser();
  if (!IsProfileMigrationEnabled(primary_user,
                                 ash::standalone_browser::migrator_util::
                                     PolicyInitState::kAfterInit)) {
    return false;
  }

  // If migration is already completed, it is not necessary to run again.
  if (ash::standalone_browser::migrator_util::
          IsProfileMigrationCompletedForUser(user_manager->GetLocalState(),
                                             primary_user->username_hash())) {
    return false;
  }

  return true;
}

bool IsAshWebBrowserEnabled() {
  return !IsLacrosEnabled();
}

bool IsLacrosOnlyBrowserAllowed() {
  if (!ash::standalone_browser::BrowserSupport::IsInitializedForPrimaryUser()) {
    // This function must be called only after user session starts.
    base::debug::DumpWithoutCrashing();
    // Returning false for compatibility.
    // TODO(crbug.com/40286020): replace this logic by CHECK/DCHECK.
    return false;
  }
  return ash::standalone_browser::BrowserSupport::GetForPrimaryUser()
      ->IsAllowed();
}

bool IsLacrosOnlyFlagAllowed() {
  return IsLacrosOnlyBrowserAllowed() &&
         // Hide lacros_only flag for guest sessions as they do always start
         // with a fresh and anonymous profile, hence ignoring this setting.
         !UserManager::Get()->IsLoggedInAsGuest() &&
         (GetCachedLacrosAvailability() == LacrosAvailability::kUserChoice);
}

bool IsLacrosAllowedToLaunch() {
  return UserManager::Get()->GetLoggedInUsers().size() == 1;
}

bool IsLacrosChromeAppsEnabled() {
  return !base::FeatureList::IsEnabled(
             ash::standalone_browser::features::kLacrosDisableChromeApps) &&
         IsLacrosEnabled();
}

bool IsLacrosEnabledInWebKioskSession() {
  return UserManager::Get()->IsLoggedInAsWebKioskApp() && IsLacrosEnabled();
}

bool IsLacrosEnabledInChromeKioskSession() {
  return UserManager::Get()->IsLoggedInAsKioskApp() && IsLacrosEnabled();
}

bool IsLacrosWindow(const aura::Window* window) {
  const std::string* app_id = exo::GetShellApplicationId(window);
  if (!app_id)
    return false;
  return base::StartsWith(*app_id, kLacrosAppIdPrefix);
}

// Assuming the metadata exists, parse the version and check if it contains the
// non-backwards-compatible account_manager change.
// A typical format for metadata is:
// {
//   "content": {
//     "version": "91.0.4469.5"
//   },
//   "metadata_version": 1
// }
bool DoesMetadataSupportNewAccountManager(base::Value* metadata) {
  if (!metadata)
    return false;

  std::string* version_str =
      metadata->GetDict().FindStringByDottedPath("content.version");
  if (!version_str) {
    return false;
  }

  std::vector<std::string> versions_str = base::SplitString(
      *version_str, ".", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  if (versions_str.size() != 4)
    return false;

  int major_version = 0;
  int minor_version = 0;
  if (!base::StringToInt(versions_str[0], &major_version))
    return false;
  if (!base::StringToInt(versions_str[2], &minor_version))
    return false;

  // TODO(crbug.com/40176822): Come up with more appropriate major/minor
  // version numbers.
  return major_version >= 1000 && minor_version >= 0;
}

base::Version GetRootfsLacrosVersionMayBlock(
    const base::FilePath& version_file_path) {
  if (!base::PathExists(version_file_path)) {
    LOG(WARNING) << "The rootfs lacros-chrome metadata is missing.";
    return {};
  }

  std::string metadata;
  if (!base::ReadFileToString(version_file_path, &metadata)) {
    PLOG(WARNING) << "Failed to read rootfs lacros-chrome metadata.";
    return {};
  }

  std::optional<base::Value> v = base::JSONReader::Read(metadata);
  if (!v || !v->is_dict()) {
    LOG(WARNING) << "Failed to parse rootfs lacros-chrome metadata.";
    return {};
  }

  const base::Value::Dict& dict = v->GetDict();
  const base::Value::Dict* content = dict.FindDict(kLacrosMetadataContentKey);
  if (!content) {
    LOG(WARNING)
        << "Failed to parse rootfs lacros-chrome metadata content key.";
    return {};
  }

  const std::string* version = content->FindString(kLacrosMetadataVersionKey);
  if (!version) {
    LOG(WARNING)
        << "Failed to parse rootfs lacros-chrome metadata version key.";
    return {};
  }

  return base::Version{*version};
}

void CacheLacrosAvailability(const policy::PolicyMap& map) {
  if (g_lacros_availability_cache.has_value()) {
    // Some browser tests might call this multiple times.
    LOG(ERROR) << "Trying to cache LacrosAvailability and the value was set";
    return;
  }

  const base::Value* value =
      map.GetValue(policy::key::kLacrosAvailability, base::Value::Type::STRING);
  g_lacros_availability_cache =
      ash::standalone_browser::DetermineLacrosAvailabilityFromPolicyValue(
          GetPrimaryUser(), value ? value->GetString() : std::string_view());
}

void CacheLacrosDataBackwardMigrationMode(const policy::PolicyMap& map) {
  if (g_lacros_data_backward_migration_mode.has_value()) {
    // Some browser tests might call this multiple times.
    LOG(ERROR) << "Trying to cache LacrosDataBackwardMigrationMode and the "
                  "value was set";
    return;
  }

  const base::Value* value = map.GetValue(
      policy::key::kLacrosDataBackwardMigrationMode, base::Value::Type::STRING);
  g_lacros_data_backward_migration_mode = ParseLacrosDataBackwardMigrationMode(
      value ? value->GetString() : std::string_view());
}

LacrosAvailability GetCachedLacrosAvailabilityForTesting() {
  return GetCachedLacrosAvailability();
}

// Returns the cached value of the LacrosDataBackwardMigrationMode policy.
LacrosDataBackwardMigrationMode GetCachedLacrosDataBackwardMigrationMode() {
  if (g_lacros_data_backward_migration_mode.has_value())
    return g_lacros_data_backward_migration_mode.value();

  // By default migration should be disabled.
  return LacrosDataBackwardMigrationMode::kNone;
}

void SetLacrosLaunchSwitchSourceForTest(LacrosAvailability test_value) {
  g_lacros_availability_cache = test_value;
}

void ClearLacrosAvailabilityCacheForTest() {
  g_lacros_availability_cache.reset();
}

void ClearLacrosDataBackwardMigrationModeCacheForTest() {
  g_lacros_data_backward_migration_mode.reset();
}

std::optional<MigrationStatus> GetMigrationStatus() {
  PrefService* local_state = g_browser_process->local_state();
  if (!local_state) {
    // This can happen in tests.
    CHECK_IS_TEST();
    return std::nullopt;
  }

  const auto* user = GetPrimaryUser();
  if (!user) {
    // The function is intended to be run after primary user is initialized.
    // The function might be run in tests without primary user being set.
    CHECK_IS_TEST();
    return std::nullopt;
  }

  return GetMigrationStatusForUser(local_state, user);
}

MigrationStatus GetMigrationStatusForUser(PrefService* local_state,
                                          const user_manager::User* user) {
  if (!crosapi::browser_util::IsLacrosEnabledForMigration(
          user, ash::standalone_browser::migrator_util::PolicyInitState::
                    kAfterInit)) {
    return MigrationStatus::kLacrosNotEnabled;
  }

  std::optional<ash::standalone_browser::migrator_util::MigrationMode> mode =
      ash::standalone_browser::migrator_util::GetCompletedMigrationMode(
          local_state, user->username_hash());

  if (!mode.has_value()) {
    if (ash::standalone_browser::migrator_util::
            IsMigrationAttemptLimitReachedForUser(local_state,
                                                  user->username_hash())) {
      return MigrationStatus::kMaxAttemptReached;
    }

    return MigrationStatus::kUncompleted;
  }

  switch (mode.value()) {
    case ash::standalone_browser::migrator_util::MigrationMode::kCopy:
      return MigrationStatus::kCopyCompleted;
    case ash::standalone_browser::migrator_util::MigrationMode::kMove:
      return MigrationStatus::kMoveCompleted;
    case ash::standalone_browser::migrator_util::MigrationMode::kSkipForNewUser:
      return MigrationStatus::kSkippedForNewUser;
  }
}

void SetProfileMigrationCompletionTimeForUser(PrefService* local_state,
                                              const std::string& user_id_hash) {
  ScopedDictPrefUpdate update(local_state,
                              kProfileMigrationCompletionTimeForUserPref);
  update->Set(user_id_hash, base::TimeToValue(base::Time::Now()));
}

std::optional<base::Time> GetProfileMigrationCompletionTimeForUser(
    PrefService* local_state,
    const std::string& user_id_hash) {
  const auto* pref =
      local_state->FindPreference(kProfileMigrationCompletionTimeForUserPref);

  if (!pref) {
    return std::nullopt;
  }

  const base::Value* value = pref->GetValue();
  DCHECK(value->is_dict());

  return base::ValueToTime(value->GetDict().Find(user_id_hash));
}

void ClearProfileMigrationCompletionTimeForUser(
    PrefService* local_state,
    const std::string& user_id_hash) {
  ScopedDictPrefUpdate update(local_state,
                              kProfileMigrationCompletionTimeForUserPref);
  base::Value::Dict& dict = update.Get();
  dict.Remove(user_id_hash);
}

void SetProfileDataBackwardMigrationCompletedForUser(
    PrefService* local_state,
    const std::string& user_id_hash) {
  ScopedDictPrefUpdate update(
      local_state, kProfileDataBackwardMigrationCompletedForUserPref);
  update->Set(user_id_hash, true);
}

void ClearProfileDataBackwardMigrationCompletedForUser(
    PrefService* local_state,
    const std::string& user_id_hash) {
  ScopedDictPrefUpdate update(
      local_state, kProfileDataBackwardMigrationCompletedForUserPref);
  base::Value::Dict& dict = update.Get();
  dict.Remove(user_id_hash);
}

LacrosLaunchSwitchSource GetLacrosLaunchSwitchSource() {
  if (!g_lacros_availability_cache.has_value())
    return LacrosLaunchSwitchSource::kUnknown;

  // Note: this check needs to be consistent with the one in
  // DetermineLacrosAvailabilityFromPolicyValue.
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(ash::switches::kLacrosAvailabilityIgnore) &&
      IsGoogleInternal(UserManager::Get()->GetPrimaryUser())) {
    return LacrosLaunchSwitchSource::kForcedByUser;
  }

  return GetCachedLacrosAvailability() == LacrosAvailability::kUserChoice
             ? LacrosLaunchSwitchSource::kPossiblySetByUser
             : LacrosLaunchSwitchSource::kForcedByPolicy;
}

std::optional<LacrosDataBackwardMigrationMode>
ParseLacrosDataBackwardMigrationMode(std::string_view value) {
  auto it = kLacrosDataBackwardMigrationModeMap.find(value);
  if (it != kLacrosDataBackwardMigrationModeMap.end())
    return it->second;

  if (!value.empty()) {
    LOG(ERROR) << "Unknown LacrosDataBackwardMigrationMode policy value: "
               << value;
  }
  return std::nullopt;
}

std::string_view GetLacrosDataBackwardMigrationModeName(
    LacrosDataBackwardMigrationMode value) {
  for (const auto& entry : kLacrosDataBackwardMigrationModeMap) {
    if (entry.second == value)
      return entry.first;
  }

  NOTREACHED_IN_MIGRATION();
  return std::string_view();
}

bool IsAshBrowserSyncEnabled() {
  // Turn off sync from Ash if Lacros is enabled and Ash web browser is
  // disabled.
  if (IsLacrosEnabled() && !IsAshWebBrowserEnabled())
    return false;

  return true;
}

void SetGotoFilesClicked(PrefService* local_state,
                         const std::string& user_id_hash) {
  ScopedListPrefUpdate update(local_state, kGotoFilesPref);
  base::Value::List& list = update.Get();
  base::Value user_id_hash_value(user_id_hash);
  if (!base::Contains(list, user_id_hash_value))
    list.Append(std::move(user_id_hash_value));
}

void ClearGotoFilesClicked(PrefService* local_state,
                           const std::string& user_id_hash) {
  ScopedListPrefUpdate update(local_state, kGotoFilesPref);
  update->EraseValue(base::Value(user_id_hash));
}

bool WasGotoFilesClicked(PrefService* local_state,
                         const std::string& user_id_hash) {
  const base::Value::List& list = local_state->GetList(kGotoFilesPref);
  return base::Contains(list, base::Value(user_id_hash));
}

bool ShouldEnforceAshExtensionKeepList() {
  return IsLacrosEnabled() && base::FeatureList::IsEnabled(
                                  ash::features::kEnforceAshExtensionKeeplist);
}

bool IsAshDevToolEnabled() {
  return IsAshWebBrowserEnabled() ||
         base::FeatureList::IsEnabled(ash::features::kAllowDevtoolsInSystemUI);
}

}  // namespace crosapi::browser_util