chromium/chrome/browser/chromeos/extensions/system_log/system_log_apitest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <string>

#include "base/files/file_path.h"
#include "base/path_service.h"
#include "base/test/test_future.h"
#include "base/values.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/mixin_based_extension_apitest.h"
#include "chrome/browser/feedback/system_logs/log_sources/device_event_log_source.h"
#include "chrome/browser/policy/extension_force_install_mixin.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_paths.h"
#include "components/device_event_log/device_event_log.h"
#include "components/feedback/system_logs/system_logs_source.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/cloud/cloud_policy_constants.h"
#include "components/policy/core/common/cloud/test/policy_builder.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/policy_constants.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/api/test/test_api.h"
#include "extensions/common/extension_api.h"
#include "extensions/common/switches.h"
#include "extensions/test/result_catcher.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/constants/ash_switches.h"
#include "chrome/browser/ash/login/test/device_state_mixin.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/test/scoped_policy_update.h"
#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
#include "chrome/browser/ash/policy/core/device_local_account.h"
#include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h"
#include "chrome/browser/ash/policy/test_support/embedded_policy_test_server_mixin.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#else  // BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/startup/browser_init_params.h"
#endif

namespace extensions {

namespace {

constexpr char kApiExtensionRelativePath[] = "extensions/api_test/system_log";
constexpr char kExtensionPemRelativePath[] =
    "extensions/api_test/system_log.pem";
// ID associated with the .pem.
constexpr char kExtensionId[] = "ghbglelacokpaehlgjbgdfmmggnihdcf";

constexpr char kDeviceEventLogEntry[] = "device_event_log";

// Test names for when chrome.systemLog is available to the extension (if policy
// installed) and when chrome.systemLog is undefined (user installed).
constexpr char kSystemLogAvailableTestName[] = "SystemLogAvailable";
constexpr char kSystemLogUndefinedTestName[] = "SystemLogUndefined";

#if BUILDFLAG(IS_CHROMEOS_ASH)
const char kManagedAccountId[] = "managed-guest-account@test";
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

void VerifyDeviceEventLogLevel(const std::string& log_level,
                               bool on_signin_screen = false) {
  const std::string produced_logs = device_event_log::GetAsString(
      device_event_log::NEWEST_FIRST, /*format=*/"level",
      /*types=*/"extensions",
      /*max_level=*/device_event_log::LOG_LEVEL_DEBUG, /*max_events=*/1);

  const std::string expected_logs =
      base::StringPrintf("%s: [%s]%s: Test log message\n", log_level.c_str(),
                         kExtensionId, on_signin_screen ? "[signin]" : "");

  EXPECT_EQ(expected_logs, produced_logs);
}

bool AreLogsForwardedToFeedbackReport(bool on_signin_screen = false) {
  auto log_source = std::make_unique<system_logs::DeviceEventLogSource>();
  base::test::TestFuture<std::unique_ptr<system_logs::SystemLogsResponse>>
      future;
  log_source->Fetch(future.GetCallback());
  EXPECT_TRUE(future.Wait());

  const std::string expected_feedback_log =
      base::StringPrintf("[%s]%s: Test log message\n", kExtensionId,
                         on_signin_screen ? "[signin]" : "");

  system_logs::SystemLogsResponse* response = future.Get().get();
  const auto device_event_log_iter = response->find(kDeviceEventLogEntry);

  return device_event_log_iter != response->end() &&
         device_event_log_iter->second.find(expected_feedback_log) !=
             std::string::npos;
}

}  // namespace

#if BUILDFLAG(IS_CHROMEOS_ASH)
// Verifies the systemLog API logs on the sign-in screen.
class SystemLogSigninScreenApitest
    : public MixinBasedExtensionApiTest,
      public ::testing::WithParamInterface<bool> {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(ash::switches::kOobeSkipPostLogin);
    command_line->AppendSwitchASCII(switches::kAllowlistedExtensionID,
                                    kExtensionId);

    MixinBasedExtensionApiTest::SetUpCommandLine(command_line);
  }

  void SetUpOnMainThread() override {
    MixinBasedExtensionApiTest::SetUpOnMainThread();
    extension_force_install_mixin_.InitWithDeviceStateMixin(
        GetOriginalSigninProfile(), &device_state_mixin_);
  }

  void SetSystemLogPolicy(bool system_logging_enabled) {
    device_state_mixin_.RequestDevicePolicyUpdate()
        ->policy_payload()
        ->mutable_deviceextensionssystemlogenabled()
        ->set_value(system_logging_enabled);
  }

  void ForceInstallExtension() {
    base::FilePath test_dir_path =
        base::PathService::CheckedGet(chrome::DIR_TEST_DATA);

    EXPECT_TRUE(extension_force_install_mixin_.ForceInstallFromSourceDir(
        test_dir_path.AppendASCII(kApiExtensionRelativePath),
        test_dir_path.AppendASCII(kExtensionPemRelativePath),
        ExtensionForceInstallMixin::WaitMode::kLoad));
  }

  Profile* GetOriginalSigninProfile() {
    return Profile::FromBrowserContext(
               ash::BrowserContextHelper::Get()->GetSigninBrowserContext())
        ->GetOriginalProfile();
  }

 private:
  ash::DeviceStateMixin device_state_mixin_{
      &mixin_host_,
      ash::DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED};
  ash::LoginManagerMixin login_manager_mixin_{&mixin_host_};
  ExtensionForceInstallMixin extension_force_install_mixin_{&mixin_host_};
};

// Logs EVENT or DEBUG extension logs depending on the
// DeviceExtensionsSystemLogEnabled policy.
IN_PROC_BROWSER_TEST_P(SystemLogSigninScreenApitest, AddLogFromSignInScreen) {
  const bool system_logging_enabled = GetParam();
  SetSystemLogPolicy(system_logging_enabled);

  SetCustomArg(kSystemLogAvailableTestName);
  ResultCatcher catcher;

  ForceInstallExtension();
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();

  const std::string log_level = system_logging_enabled ? "DEBUG" : "EVENT";
  VerifyDeviceEventLogLevel(log_level, /*on_signin_screen=*/true);

  // Logs are forwarded to the feedback report if they are added to the device
  // event log with an EVENT log level. Otherwise the logs will be added to the
  // feedback report via the system log file.
  EXPECT_EQ(AreLogsForwardedToFeedbackReport(/*on_signin_screen=*/true),
            !system_logging_enabled);
}

INSTANTIATE_TEST_SUITE_P(All,
                         SystemLogSigninScreenApitest,
                         /*system_logging_enabled=*/testing::Bool());
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

// Verifies the systemLog API logs in user sessions.
class SystemLogUserSessionApitest : public MixinBasedExtensionApiTest,
                                    public ::testing::WithParamInterface<bool> {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    MixinBasedExtensionApiTest::SetUpCommandLine(command_line);

    command_line->AppendSwitchASCII(switches::kAllowlistedExtensionID,
                                    kExtensionId);
  }

  void SetUpInProcessBrowserTestFixture() override {
    MixinBasedExtensionApiTest::SetUpInProcessBrowserTestFixture();

    mock_policy_provider_.SetDefaultReturns(
        /*is_initialization_complete_return=*/true,
        /*is_first_policy_load_complete_return=*/true);
    mock_policy_provider_.SetAutoRefresh();
    policy::BrowserPolicyConnector::SetPolicyProviderForTesting(
        &mock_policy_provider_);
  }

  void SetUpOnMainThread() override {
    extension_force_install_mixin_.InitWithMockPolicyProvider(
        profile(), &mock_policy_provider_);

    MixinBasedExtensionApiTest::SetUpOnMainThread();
  }

  void SetSystemLogPolicy(bool system_logging_enabled) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
    policy::PolicyMap policy_map =
        mock_policy_provider_.policies()
            .Get(policy::PolicyNamespace(policy::POLICY_DOMAIN_CHROME,
                                         /*component_id=*/std::string()))
            .Clone();
    policy_map.Set(policy::key::kDeviceExtensionsSystemLogEnabled,
                   policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_MACHINE,
                   policy::POLICY_SOURCE_CLOUD, base::Value(true), nullptr);
    mock_policy_provider_.UpdateChromePolicy(policy_map);
#else  // BUILDFLAG(IS_CHROMEOS_LACROS)
    auto params = chromeos::BrowserInitParams::GetForTests()->Clone();
    params->session_type = crosapi::mojom::SessionType::kRegularSession;
    params->device_settings = crosapi::mojom::DeviceSettings::New();
    params->device_settings->device_extensions_system_log_enabled =
        system_logging_enabled
            ? crosapi::mojom::DeviceSettings::OptionalBool::kTrue
            : crosapi::mojom::DeviceSettings::OptionalBool::kFalse;
    chromeos::BrowserInitParams::SetInitParamsForTests(std::move(params));
#endif
  }

  void ForceInstallExtension() {
    base::FilePath test_dir_path =
        base::PathService::CheckedGet(chrome::DIR_TEST_DATA);

    EXPECT_TRUE(extension_force_install_mixin_.ForceInstallFromSourceDir(
        test_dir_path.AppendASCII(kApiExtensionRelativePath),
        test_dir_path.AppendASCII(kExtensionPemRelativePath),
        ExtensionForceInstallMixin::WaitMode::kLoad));
  }

 private:
  ExtensionForceInstallMixin extension_force_install_mixin_{&mixin_host_};
  testing::NiceMock<policy::MockConfigurationPolicyProvider>
      mock_policy_provider_;
};

// Logs EVENT extension logs irrespective of the
// DeviceExtensionsSystemLogEnabled policy.
IN_PROC_BROWSER_TEST_P(SystemLogUserSessionApitest, AddLogFromUserSession) {
  const bool system_logging_enabled = GetParam();
  SetSystemLogPolicy(system_logging_enabled);

  SetCustomArg(kSystemLogAvailableTestName);
  ResultCatcher catcher;

  ForceInstallExtension();
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();

  // Logs are always added to the device event log buffer with the EVENT level.
  VerifyDeviceEventLogLevel("EVENT");

  // Logs are always forwarded to the feedback report via the device event log
  // buffer.
  EXPECT_TRUE(AreLogsForwardedToFeedbackReport());
}

IN_PROC_BROWSER_TEST_P(SystemLogUserSessionApitest,
                       DeniesNonPolicyInstalledExtensions) {
  const bool system_logging_enabled = GetParam();
  SetSystemLogPolicy(system_logging_enabled);

  SetCustomArg(kSystemLogUndefinedTestName);
  ResultCatcher catcher;

  // Add user installed extension.
  extensions::ChromeTestExtensionLoader loader(profile());
  base::FilePath extension_path =
      base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
          .AppendASCII(kApiExtensionRelativePath);
  loader.set_location(extensions::mojom::ManifestLocation::kInternal);
  loader.set_pack_extension(true);
  loader.set_ignore_manifest_warnings(true);
  loader.LoadExtension(extension_path);

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

INSTANTIATE_TEST_SUITE_P(All,
                         SystemLogUserSessionApitest,
                         /*system_logging_enabled=*/testing::Bool());

// Verifies the systemLog API logs in managed guest sessions.
#if BUILDFLAG(IS_CHROMEOS_ASH)
class SystemLogManagedGuestSessionApitest
    : public policy::DevicePolicyCrosBrowserTest,
      public ::testing::WithParamInterface<bool> {
#else  // BUILDFLAG(IS_CHROMEOS_LACROS)
class SystemLogManagedGuestSessionApitest
    : public MixinBasedExtensionApiTest,
      public ::testing::WithParamInterface<bool> {
#endif

 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
#if BUILDFLAG(IS_CHROMEOS_ASH)
    DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(ash::switches::kLoginManager);
    command_line->AppendSwitch(ash::switches::kForceLoginManagerInTests);
    command_line->AppendSwitch(ash::switches::kOobeSkipPostLogin);
#else  // BUILDFLAG(IS_CHROMEOS_LACROS)
    MixinBasedExtensionApiTest::SetUpCommandLine(command_line);
#endif
    command_line->AppendSwitchASCII(switches::kAllowlistedExtensionID,
                                    kExtensionId);
  }

#if BUILDFLAG(IS_CHROMEOS_ASH)
  void SetSystemLogPolicy(bool system_logging_enabled) {
    SetDevicePolicies(system_logging_enabled);
    ash::test::WaitForPrimaryUserSessionStart();
    Profile* profile = GetActiveUserProfile();

    SetUpUserPolicyBuilderForPublicAccount(&user_policy_builder_);

    extension_force_install_mixin_.InitWithEmbeddedPolicyMixin(
        profile, &policy_test_server_mixin_, &user_policy_builder_,
        kManagedAccountId, policy::dm_protocol::kChromePublicAccountPolicyType);
  }

  void SetDevicePolicies(bool system_logging_enabled) {
    enterprise_management::ChromeDeviceSettingsProto& proto(
        device_policy()->payload());
    // Set Managed Guest Session policy.
    enterprise_management::DeviceLocalAccountsProto* device_local_accounts =
        proto.mutable_device_local_accounts();
    enterprise_management::DeviceLocalAccountInfoProto* const account =
        device_local_accounts->add_account();
    account->set_account_id(kManagedAccountId);
    account->set_type(enterprise_management::DeviceLocalAccountInfoProto::
                          ACCOUNT_TYPE_PUBLIC_SESSION);
    device_local_accounts->set_auto_login_id(kManagedAccountId);
    device_local_accounts->set_auto_login_delay(0);

    // Set System Log policy.
    proto.mutable_deviceextensionssystemlogenabled()->set_value(
        system_logging_enabled);
    RefreshDevicePolicy();
    policy_test_server_mixin_.UpdateDevicePolicy(proto);
  }

  void SetUpUserPolicyBuilderForPublicAccount(
      policy::UserPolicyBuilder* user_policy_builder) {
    enterprise_management::PolicyData& policy_data =
        user_policy_builder->policy_data();
    policy_data.set_public_key_version(1);
    policy_data.set_policy_type(
        policy::dm_protocol::kChromePublicAccountPolicyType);
    policy_data.set_username(kManagedAccountId);
    policy_data.set_settings_entity_id(kManagedAccountId);
    user_policy_builder->SetDefaultSigningKey();
  }

  Profile* GetActiveUserProfile() {
    const user_manager::User* active_user =
        user_manager::UserManager::Get()->GetActiveUser();
    return Profile::FromBrowserContext(
        ash::BrowserContextHelper::Get()->GetBrowserContextByUser(active_user));
  }
#else  // BUILDFLAG(IS_CHROMEOS_LACROS)
  void SetUpInProcessBrowserTestFixture() override {
    MixinBasedExtensionApiTest::SetUpInProcessBrowserTestFixture();

    mock_policy_provider_.SetDefaultReturns(
        /*is_initialization_complete_return=*/true,
        /*is_first_policy_load_complete_return=*/true);
    mock_policy_provider_.SetAutoRefresh();
    policy::BrowserPolicyConnector::SetPolicyProviderForTesting(
        &mock_policy_provider_);
  }

  void SetUpOnMainThread() override {
    extension_force_install_mixin_.InitWithMockPolicyProvider(
        profile(), &mock_policy_provider_);

    MixinBasedExtensionApiTest::SetUpOnMainThread();
  }

  void SetSystemLogPolicy(bool system_logging_enabled) {
    auto params = chromeos::BrowserInitParams::GetForTests()->Clone();
    params->session_type = crosapi::mojom::SessionType::kPublicSession;
    params->device_settings = crosapi::mojom::DeviceSettings::New();
    params->device_settings->device_extensions_system_log_enabled =
        system_logging_enabled
            ? crosapi::mojom::DeviceSettings::OptionalBool::kTrue
            : crosapi::mojom::DeviceSettings::OptionalBool::kFalse;
    chromeos::BrowserInitParams::SetInitParamsForTests(std::move(params));
  }
#endif

  void ForceInstallExtension() {
    base::FilePath test_dir_path =
        base::PathService::CheckedGet(chrome::DIR_TEST_DATA);

    EXPECT_TRUE(extension_force_install_mixin_.ForceInstallFromSourceDir(
        test_dir_path.AppendASCII(kApiExtensionRelativePath),
        test_dir_path.AppendASCII(kExtensionPemRelativePath),
        ExtensionForceInstallMixin::WaitMode::kLoad));
  }

  void SetTestCustomArg(const std::string custom_arg) {
    config_.Set("customArg", base::Value(custom_arg));
    extensions::TestGetConfigFunction::set_test_config_state(&config_);
  }

 protected:
#if BUILDFLAG(IS_CHROMEOS_ASH)
  ash::EmbeddedPolicyTestServerMixin policy_test_server_mixin_{&mixin_host_};
  policy::UserPolicyBuilder user_policy_builder_;
#else  // BUILDFLAG(IS_CHROMEOS_LACROS)
  testing::NiceMock<policy::MockConfigurationPolicyProvider>
      mock_policy_provider_;
#endif
  base::Value::Dict config_;
  ExtensionForceInstallMixin extension_force_install_mixin_{&mixin_host_};
};

// Logs EVENT or DEBUG extension logs depending on the
// DeviceExtensionsSystemLogEnabled policy.
IN_PROC_BROWSER_TEST_P(SystemLogManagedGuestSessionApitest,
                       AddLogFromManagedGuestSession) {
  const bool system_logging_enabled = GetParam();
  SetSystemLogPolicy(system_logging_enabled);

  SetTestCustomArg(kSystemLogAvailableTestName);
  ResultCatcher catcher;

  ForceInstallExtension();
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();

  const std::string log_level = system_logging_enabled ? "DEBUG" : "EVENT";
  VerifyDeviceEventLogLevel(log_level);

  // Logs are forwarded to the feedback report if they are added to the device
  // event log with an EVENT log level. Otherwise the logs will be added to the
  // feedback report via the system log file.
  EXPECT_EQ(AreLogsForwardedToFeedbackReport(), !system_logging_enabled);
}

INSTANTIATE_TEST_SUITE_P(All,
                         SystemLogManagedGuestSessionApitest,
                         /*system_logging_enabled=*/testing::Bool());
}  // namespace extensions