// 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