chromium/chrome/browser/ash/policy/handlers/site_isolation_flag_handling_browsertest.cc

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

#include <ostream>
#include <set>
#include <string>
#include <vector>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ash/login/existing_user_controller.h"
#include "chrome/browser/ash/login/session/user_session_manager.h"
#include "chrome/browser/ash/login/session/user_session_manager_test_api.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/oobe_base_test.h"
#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
#include "chrome/browser/ash/login/test/user_policy_mixin.h"
#include "chrome/browser/ash/login/ui/login_display_host.h"
#include "chrome/browser/ash/login/users/chrome_user_manager_impl.h"
#include "chrome/browser/ash/login/wizard_controller.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/site_isolation/about_flags.h"
#include "chrome/browser/ui/webui/ash/login/gaia_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/oobe_ui.h"
#include "chrome/test/base/fake_gaia_mixin.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
#include "chromeos/ash/components/dbus/session_manager/fake_session_manager_client.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "components/flags_ui/pref_service_flags_storage.h"
#include "components/policy/core/browser/browser_policy_connector.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 "components/session_manager/core/session_manager.h"
#include "components/session_manager/core/session_manager_observer.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/child_process_security_policy.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/cros_system_api/switches/chrome_switches.h"

namespace policy {
namespace {

namespace em = ::enterprise_management;

struct Params {
  Params(std::string login_screen_isolate_origins,
         std::string user_policy_isolate_origins,
         bool user_policy_site_per_process,
         std::vector<std::string> user_flag_internal_names,
         bool ephemeral_users,
         bool expected_request_restart,
         std::vector<std::string> expected_switches_for_user,
         std::vector<std::string> expected_isolated_origins = {})
      : login_screen_isolate_origins(login_screen_isolate_origins),
        user_policy_isolate_origins(user_policy_isolate_origins),
        user_policy_site_per_process(user_policy_site_per_process),
        user_flag_internal_names(std::move(user_flag_internal_names)),
        ephemeral_users(ephemeral_users),
        expected_request_restart(expected_request_restart),
        expected_switches_for_user(expected_switches_for_user),
        expected_isolated_origins(expected_isolated_origins) {}

  friend std::ostream& operator<<(std::ostream& os, const Params& p) {
    os << "{" << std::endl
       << "  login_screen_isolate_origins: " << p.login_screen_isolate_origins
       << std::endl
       << "  user_policy_site_per_process: " << p.user_policy_site_per_process
       << std::endl
       << "  user_policy_isolate_origins: " << p.user_policy_isolate_origins
       << std::endl
       << "  user_flag_internal_names: "
       << base::JoinString(p.user_flag_internal_names, ", ") << std::endl
       << "  ephemeral_users: " << p.ephemeral_users << std::endl
       << "  expected_request_restart: " << p.expected_request_restart
       << std::endl
       << "  expected_switches_for_user: "
       << base::JoinString(p.expected_switches_for_user, ", ") << std::endl
       << "  expected_isolated_origins: "
       << base::JoinString(p.expected_isolated_origins, ", ") << std::endl
       << "}";
    return os;
  }

  // If non-empty, --isolate-origins=|login_screen_isolate_origins| will be
  // passed to the login manager chrome instance between policy flag sentinels.
  // Note: On Chrome OS, login_manager evaluates device policy and does this.
  std::string login_screen_isolate_origins;

  // If non-empty, the IsolateOrigins user policy will be simulated to be set
  // |user_policy_isolate_origins|.
  std::string user_policy_isolate_origins;

  // If true, the SitePerProcess user policy will be simulated to be set to
  // true.
  bool user_policy_site_per_process;

  std::vector<std::string> user_flag_internal_names;

  // If true, ephemeral users are enabled.
  bool ephemeral_users;

  // If true, the test case will expect that AttemptRestart has been called by
  // UserSessionManager.
  bool expected_request_restart;

  // When a restart was requested, the test case verifies that the flags passed
  // to |SessionManagerClient::SetFlagsForUser| match
  // |expected_switches_for_user|.
  std::vector<std::string> expected_switches_for_user;

  // List of origins that should be isolated (via policy or via cmdline flag).
  std::vector<std::string> expected_isolated_origins;
};

// Defines the test cases that will be executed.
const Params kTestCases[] = {
    // 0. No site isolation in device or user policy - no restart expected.
    Params(std::string() /* login_screen_isolate_origins */,
           std::string() /* user_policy_isolate_origins */,
           false /* user_policy_site_per_process */,
           {} /* user_flag_internal_names */,
           false /* ephemeral_users */,
           false /* expected_request_restart */,
           {} /* expected_switches_for_user */),

    // 1. SitePerProcess opt-out through about://flags - restart expected.
    Params(
        std::string() /* login_screen_isolate_origins */,
        std::string() /* user_policy_isolate_origins */,
        false /* user_policy_site_per_process */,
        /* user_flag_internal_names */
        {about_flags::SiteIsolationTrialOptOutChoiceEnabled()},
        false /* ephemeral_users */,
        true /* expected_request_restart */,
        {"--disable-site-isolation-trials"} /* expected_switches_for_user */),

    // 2. SitePerProcess forced through user policy - opt-out through
    // about://flags entry expected to be ignored.
    Params(std::string() /* login_screen_isolate_origins */,
           std::string() /* user_policy_isolate_origins */,
           true /* user_policy_site_per_process */,
           /* user_flag_internal_names */
           {about_flags::SiteIsolationTrialOptOutChoiceEnabled()},
           false /* ephemeral_users */,
           false /* expected_request_restart */,
           {} /* expected_switches_for_user */),

    // 3. IsolateOrigins in user policy only - no restart expected, because
    //    IsolateOrigins from the user policy should be picked up by
    //    SiteIsolationPrefsObserver (without requiring injection of the
    //    --isolate-origins cmdline switch).
    Params(std::string() /* login_screen_isolate_origins */,
           "https://example.com" /* user_policy_isolate_origins */,
           false /* user_policy_site_per_process */,
           {} /* user_flag_internal_names */,
           false /* ephemeral_users */,
           false /* expected_request_restart */,
           {} /* expected_switches_for_user */,
           {"https://example.com"} /* expected_isolated_origins */)};

class SiteIsolationFlagHandlingTest
    : public ash::OobeBaseTest,
      public ::testing::WithParamInterface<Params> {
 public:
  SiteIsolationFlagHandlingTest(const SiteIsolationFlagHandlingTest&) = delete;
  SiteIsolationFlagHandlingTest& operator=(
      const SiteIsolationFlagHandlingTest&) = delete;

 protected:
  SiteIsolationFlagHandlingTest()
      : account_id_(AccountId::FromUserEmailGaiaId("[email protected]",
                                                   "1111111111")) {}

  void SetUpInProcessBrowserTestFixture() override {
    ash::SessionManagerClient::InitializeFakeInMemory();

    // Mark that chrome restart can be requested.
    // Note that AttemptRestart() is mocked out in UserSessionManager through
    // |SetAttemptRestartClosureInTests| (set up in SetUpOnMainThread).
    ash::FakeSessionManagerClient::Get()->set_supports_browser_restart(true);

    std::unique_ptr<ash::ScopedDevicePolicyUpdate> update =
        device_state_.RequestDevicePolicyUpdate();
    update->policy_payload()
        ->mutable_ephemeral_users_enabled()
        ->set_ephemeral_users_enabled(GetParam().ephemeral_users);
    update.reset();

    std::unique_ptr<ash::ScopedUserPolicyUpdate> user_policy_update =
        user_policy_.RequestPolicyUpdate();
    if (GetParam().user_policy_site_per_process) {
      user_policy_update->policy_payload()
          ->mutable_siteperprocess()
          ->mutable_policy_options()
          ->set_mode(em::PolicyOptions::MANDATORY);
      user_policy_update->policy_payload()->mutable_siteperprocess()->set_value(
          true);
    }

    if (!GetParam().user_policy_isolate_origins.empty()) {
      user_policy_update->policy_payload()
          ->mutable_isolateorigins()
          ->mutable_policy_options()
          ->set_mode(em::PolicyOptions::MANDATORY);
      user_policy_update->policy_payload()->mutable_isolateorigins()->set_value(
          GetParam().user_policy_isolate_origins);
    }
    user_policy_update.reset();

    OobeBaseTest::SetUpInProcessBrowserTestFixture();
  }

  void SetUpOnMainThread() override {
    fake_gaia_.SetupFakeGaiaForLogin(account_id_.GetUserEmail(),
                                     account_id_.GetGaiaId(),
                                     FakeGaiaMixin::kFakeRefreshToken);

    OobeBaseTest::SetUpOnMainThread();

    // Mock out chrome restart.
    ash::test::UserSessionManagerTestApi session_manager_test_api(
        ash::UserSessionManager::GetInstance());
    session_manager_test_api.SetAttemptRestartClosureInTests(
        base::BindRepeating(
            &SiteIsolationFlagHandlingTest::AttemptRestartCalled,
            base::Unretained(this)));

    // Observe for user session start.
    user_session_started_observer_ =
        std::make_unique<ash::SessionStateWaiter>();
  }

  ash::ChromeUserManagerImpl* GetChromeUserManager() const {
    return static_cast<ash::ChromeUserManagerImpl*>(
        user_manager::UserManager::Get());
  }

  bool HasAttemptRestartBeenCalled() const { return attempt_restart_called_; }

  // Called when chrome requests a restarted.
  void AttemptRestartCalled() {
    user_session_started_observer_.reset();
    attempt_restart_called_ = true;
  }

  void LogIn() {
    // Start user sign-in. We can't use |LoginPolicyTestBase::LogIn|, because
    // it waits for a user session start unconditionally, which will not happen
    // if chrome requests a restart to set user-session flags.
    login_manager_.SkipPostLoginScreens();
    OobeBaseTest::WaitForSigninScreen();
    login_manager_.LoginWithDefaultContext(user_);

    // Wait for either the user session to start, or for restart to be requested
    // (whichever happens first).
    user_session_started_observer_->Wait();
  }

  const AccountId account_id_;

  // This will be set to |true| when chrome has requested a restart.
  bool attempt_restart_called_ = false;

  // This is important because ephemeral users only work on enrolled machines.
  ash::DeviceStateMixin device_state_{
      &mixin_host_,
      ash::DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED};
  ash::UserPolicyMixin user_policy_{&mixin_host_, account_id_};

  const ash::LoginManagerMixin::TestUserInfo user_{account_id_};
  ash::LoginManagerMixin login_manager_{&mixin_host_, {user_}};

  FakeGaiaMixin fake_gaia_{&mixin_host_};

  // Observes for user session start.
  std::unique_ptr<ash::SessionStateWaiter> user_session_started_observer_;
};

}  // namespace

IN_PROC_BROWSER_TEST_P(SiteIsolationFlagHandlingTest, PRE_FlagHandlingTest) {
  LogIn();

  if (!GetParam().user_flag_internal_names.empty()) {
    Profile* profile = ash::ProfileHelper::Get()->GetProfileByUser(
        user_manager::UserManager::Get()->GetActiveUser());
    ASSERT_TRUE(profile);
    flags_ui::PrefServiceFlagsStorage flags_storage(profile->GetPrefs());
    std::set<std::string> flags_to_set;
    for (const std::string& flag_to_set : GetParam().user_flag_internal_names)
      flags_to_set.insert(flag_to_set);
    EXPECT_TRUE(flags_storage.SetFlags(flags_to_set));
    flags_storage.CommitPendingWrites();
  }
}

IN_PROC_BROWSER_TEST_P(SiteIsolationFlagHandlingTest, FlagHandlingTest) {
  // Skip tests where expected_request_restart is true.
  // See crbug.com/990817 for more details.
  if (GetParam().expected_request_restart)
    return;

  // Log in and wait for either the user session to start, or for the restart
  // to be requested (whichever happens first).
  LogIn();

  EXPECT_EQ(GetParam().expected_request_restart, HasAttemptRestartBeenCalled());

  // Verify that expected origins are isolated...
  auto* policy = content::ChildProcessSecurityPolicy::GetInstance();
  for (const std::string& origin_str : GetParam().expected_isolated_origins) {
    url::Origin origin = url::Origin::Create(GURL(origin_str));
    EXPECT_TRUE(policy->IsGloballyIsolatedOriginForTesting(origin));
  }

  if (!HasAttemptRestartBeenCalled())
    return;

  // Also verify flags if chrome was restarted.
  std::vector<std::string> switches_for_user;
  bool has_switches_for_user =
      ash::FakeSessionManagerClient::Get()->GetFlagsForUser(
          cryptohome::CreateAccountIdentifierFromAccountId(account_id_),
          &switches_for_user);
  EXPECT_TRUE(has_switches_for_user);

  // Remove flag sentinels. Keep whatever is between those sentinels, to
  // verify that we don't pass additional parameters in there.
  std::erase_if(switches_for_user, [](const std::string& flag) {
    return flag == "--flag-switches-begin" || flag == "--flag-switches-end";
  });
  EXPECT_EQ(GetParam().expected_switches_for_user, switches_for_user);
}

INSTANTIATE_TEST_SUITE_P(All,
                         SiteIsolationFlagHandlingTest,
                         ::testing::ValuesIn(kTestCases));

}  // namespace policy