chromium/chrome/browser/ash/exo/chrome_security_delegate_unittest.cc

// Copyright 2024 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/exo/chrome_security_delegate.h"

#include <string>
#include <vector>

#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_security_delegate.h"
#include "chrome/browser/ash/crostini/crostini_test_helper.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/guest_os/guest_os_security_delegate.h"
#include "chrome/browser/ash/guest_os/guest_os_share_path.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/dbus/chunneld/chunneld_client.h"
#include "chromeos/ash/components/dbus/cicerone/cicerone_client.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/dbus/seneschal/fake_seneschal_client.h"
#include "chromeos/ash/components/dbus/seneschal/seneschal_client.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "content/public/test/browser_task_environment.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/window_types.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/test/test_windows.h"
#include "ui/aura/window.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/compositor/layer_type.h"
#include "ui/gfx/geometry/rect.h"

namespace ash {

namespace {

std::vector<uint8_t> Data(const std::string& s) {
  return std::vector<uint8_t>(s.begin(), s.end());
}

void Capture(std::string* result, scoped_refptr<base::RefCountedMemory> data) {
  *result = std::string(base::as_string_view(*data));
}

void CaptureUTF16(std::string* result,
                  scoped_refptr<base::RefCountedMemory> data) {
  base::span<const uint8_t> bytes = *data;
  std::u16string str(bytes.size() / 2u, u'\0');
  base::as_writable_byte_span(str).copy_from(bytes);
  *result = base::UTF16ToUTF8(str);
}

}  // namespace

class ChromeSecurityDelegateTest : public testing::Test {
 public:
  void SetUp() override {
    ChunneldClient::InitializeFake();
    CiceroneClient::InitializeFake();
    ConciergeClient::InitializeFake();
    SeneschalClient::InitializeFake();

    profile_ = std::make_unique<TestingProfile>();
    test_helper_ =
        std::make_unique<crostini::CrostiniTestHelper>(profile_.get());

    // Setup CrostiniManager for testing.
    crostini::CrostiniManager* crostini_manager =
        crostini::CrostiniManager::GetForProfile(profile_.get());
    crostini_manager->AddRunningVmForTesting(crostini::kCrostiniDefaultVmName);
    crostini_manager->AddRunningContainerForTesting(
        crostini::kCrostiniDefaultVmName,
        crostini::ContainerInfo(crostini::kCrostiniDefaultContainerName,
                                "testuser", "/home/testuser",
                                "PLACEHOLDER_IP"));

    // Register MyFiles and Crostini.
    mount_points_ = storage::ExternalMountPoints::GetSystemInstance();
    // Downloads-test%40example.com-hash
    myfiles_mount_name_ =
        file_manager::util::GetDownloadsMountPointName(profile_.get());
    // $HOME/Downloads
    myfiles_dir_ =
        file_manager::util::GetMyFilesFolderForProfile(profile_.get());
    mount_points_->RegisterFileSystem(
        myfiles_mount_name_, storage::kFileSystemTypeLocal,
        storage::FileSystemMountOption(), myfiles_dir_);
    // crostini_test_termina_penguin
    crostini_mount_name_ =
        file_manager::util::GetCrostiniMountPointName(profile_.get());
    // /media/fuse/crostini_test_termina_penguin
    crostini_dir_ =
        file_manager::util::GetCrostiniMountDirectory(profile_.get());
    mount_points_->RegisterFileSystem(
        crostini_mount_name_, storage::kFileSystemTypeLocal,
        storage::FileSystemMountOption(), crostini_dir_);
  }

  void TearDown() override {
    mount_points_->RevokeAllFileSystems();
    test_helper_.reset();
    profile_.reset();
    SeneschalClient::Shutdown();
    ConciergeClient::Shutdown();
    CiceroneClient::Shutdown();
    ChunneldClient::Shutdown();
  }

 protected:
  Profile* profile() { return profile_.get(); }

  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfile> profile_;
  std::unique_ptr<crostini::CrostiniTestHelper> test_helper_;

  raw_ptr<storage::ExternalMountPoints> mount_points_;
  std::string myfiles_mount_name_;
  base::FilePath myfiles_dir_;
  std::string crostini_mount_name_;
  base::FilePath crostini_dir_;
};

TEST_F(ChromeSecurityDelegateTest, CanLockPointer) {
  auto security_delegate = std::make_unique<ChromeSecurityDelegate>();
  aura::Window container_window(nullptr, aura::client::WINDOW_TYPE_NORMAL);
  container_window.Init(ui::LAYER_NOT_DRAWN);
  aura::test::TestWindowDelegate delegate;

  // CanLockPointer should be allowed for arc and lacros, but not others.
  std::unique_ptr<aura::Window> arc_toplevel(
      aura::test::CreateTestWindowWithDelegate(&delegate, 0, gfx::Rect(),
                                               &container_window));
  arc_toplevel->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
  EXPECT_TRUE(security_delegate->CanLockPointer(arc_toplevel.get()));

  std::unique_ptr<aura::Window> lacros_toplevel(
      aura::test::CreateTestWindowWithDelegate(&delegate, 0, gfx::Rect(),
                                               &container_window));
  lacros_toplevel->SetProperty(chromeos::kAppTypeKey,
                               chromeos::AppType::LACROS);
  EXPECT_TRUE(security_delegate->CanLockPointer(lacros_toplevel.get()));

  std::unique_ptr<aura::Window> crostini_toplevel(
      aura::test::CreateTestWindowWithDelegate(&delegate, 0, gfx::Rect(),
                                               &container_window));
  crostini_toplevel->SetProperty(chromeos::kAppTypeKey,
                                 chromeos::AppType::CROSTINI_APP);
  EXPECT_FALSE(security_delegate->CanLockPointer(crostini_toplevel.get()));
}

TEST_F(ChromeSecurityDelegateTest, GetFilenames) {
  ChromeSecurityDelegate security_delegate;
  base::FilePath shared_path = myfiles_dir_.Append("shared");
  auto* guest_os_share_path =
      guest_os::GuestOsSharePath::GetForProfile(profile());
  guest_os_share_path->RegisterSharedPath(crostini::kCrostiniDefaultVmName,
                                          shared_path);
  guest_os_share_path->RegisterSharedPath(plugin_vm::kPluginVmName,
                                          shared_path);
  guest_os_share_path->RegisterSharedPath(bruschetta::kBruschettaVmName,
                                          shared_path);

  // Multiple lines should be parsed.
  // Arc should not translate paths.
  std::vector<ui::FileInfo> files = security_delegate.GetFilenames(
      ui::EndpointType::kArc,
      Data("\n\tfile:///file1\t\r\n#ignore\r\nfile:///file2\r\n"));
  EXPECT_EQ(2u, files.size());
  EXPECT_EQ("/file1", files[0].path.value());
  EXPECT_EQ("", files[0].display_name.value());
  EXPECT_EQ("/file2", files[1].path.value());
  EXPECT_EQ("", files[1].display_name.value());

  // Crostini shared paths should be mapped.
  guest_os::GuestOsSecurityDelegate crostini_security_delegate("termina");
  files = crostini_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini,
      Data("file:///mnt/chromeos/MyFiles/shared/file"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ(shared_path.Append("file"), files[0].path);

  // Crostini homedir should be mapped.
  files = crostini_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini, Data("file:///home/testuser/file"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ(crostini_dir_.Append("file"), files[0].path);

  // Crostini internal paths should be mapped.
  files = crostini_security_delegate.GetFilenames(ui::EndpointType::kCrostini,
                                                  Data("file:///etc/hosts"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ("vmfile:termina:/etc/hosts", files[0].path.value());

  // Unshared paths should fail.
  files = crostini_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini,
      Data("file:///mnt/chromeos/MyFiles/unshared/file"));
  EXPECT_EQ(0u, files.size());
  files = crostini_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini,
      Data("file:///mnt/chromeos/MyFiles/shared/file1\r\n"
           "file:///mnt/chromeos/MyFiles/unshared/file2"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ(shared_path.Append("file1"), files[0].path);

  // file:/path should fail.
  files = crostini_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini, Data("file:/mnt/chromeos/MyFiles/file"));
  EXPECT_EQ(0u, files.size());

  // file:path should fail.
  files = crostini_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini, Data("file:mnt/chromeos/MyFiles/file"));
  EXPECT_EQ(0u, files.size());

  // file:// should fail.
  files = crostini_security_delegate.GetFilenames(ui::EndpointType::kCrostini,
                                                  Data("file://"));
  EXPECT_EQ(0u, files.size());

  // file:/// maps to internal root.
  files = crostini_security_delegate.GetFilenames(ui::EndpointType::kCrostini,
                                                  Data("file:///"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ("vmfile:termina:/", files[0].path.value());

  // /path should fail.
  files = crostini_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini, Data("/mnt/chromeos/MyFiles/file"));
  EXPECT_EQ(0u, files.size());

  // Plugin VM shared paths should be mapped.
  files = security_delegate.GetFilenames(
      ui::EndpointType::kPluginVm, Data("file://ChromeOS/MyFiles/shared/file"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ(shared_path.Append("file"), files[0].path);

  // Plugin VM internal paths should be mapped.
  files = security_delegate.GetFilenames(
      ui::EndpointType::kPluginVm, Data("file:///C:/WINDOWS/notepad.exe"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ("vmfile:PvmDefault:C:/WINDOWS/notepad.exe", files[0].path.value());

  // Unshared paths should fail.
  files = security_delegate.GetFilenames(
      ui::EndpointType::kPluginVm,
      Data("file://ChromeOS/MyFiles/unshared/file"));
  EXPECT_EQ(0u, files.size());
  files = security_delegate.GetFilenames(
      ui::EndpointType::kPluginVm,
      Data("file://ChromeOS/MyFiles/shared/file1\r\n"
           "file://ChromeOS/MyFiles/unshared/file2"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ(shared_path.Append("file1"), files[0].path);

  // Bruschetta shared paths should be mapped.
  guest_os::GuestOsSecurityDelegate bru_security_delegate("bru");
  files = bru_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini,
      Data("file:///mnt/shared/MyFiles/shared/file"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ(shared_path.Append("file"), files[0].path);

  // Bruschetta homedir is mapped as an internal path.
  files = bru_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini, Data("file:///home/testuser/file"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ("vmfile:bru:/home/testuser/file", files[0].path.value());

  // Bruschetta internal paths should be mapped.
  files = bru_security_delegate.GetFilenames(ui::EndpointType::kCrostini,
                                             Data("file:///etc/hosts"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ("vmfile:bru:/etc/hosts", files[0].path.value());

  // Unshared paths should fail.
  files = bru_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini,
      Data("file:///mnt/shared/MyFiles/unshared/file"));
  EXPECT_EQ(0u, files.size());
  files = bru_security_delegate.GetFilenames(
      ui::EndpointType::kCrostini,
      Data("file:///mnt/shared/MyFiles/shared/file1\r\n"
           "file:///mnt/shared/MyFiles/unshared/file2"));
  EXPECT_EQ(1u, files.size());
  EXPECT_EQ(shared_path.Append("file1"), files[0].path);
}

TEST_F(ChromeSecurityDelegateTest, SendFileInfoConvertPaths) {
  ChromeSecurityDelegate security_delegate;
  ui::FileInfo file1(myfiles_dir_.Append("file1"), base::FilePath());
  ui::FileInfo file2(myfiles_dir_.Append("file2"), base::FilePath());
  auto* guest_os_share_path =
      guest_os::GuestOsSharePath::GetForProfile(profile());
  guest_os_share_path->RegisterSharedPath(plugin_vm::kPluginVmName,
                                          myfiles_dir_);

  // Arc should convert path to UTF16 URL.
  std::string data;
  security_delegate.SendFileInfo(ui::EndpointType::kArc, {file1},
                                 base::BindOnce(&CaptureUTF16, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ(
      "content://org.chromium.arc.volumeprovider/"
      "0000000000000000000000000000CAFEF00D2019/file1",
      data);

  // Arc should join lines with CRLF.
  security_delegate.SendFileInfo(ui::EndpointType::kArc, {file1, file2},
                                 base::BindOnce(&CaptureUTF16, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ(
      "content://org.chromium.arc.volumeprovider/"
      "0000000000000000000000000000CAFEF00D2019/file1"
      "\r\n"
      "content://org.chromium.arc.volumeprovider/"
      "0000000000000000000000000000CAFEF00D2019/file2",
      data);

  // Crostini should convert path to inside VM, and share the path.
  guest_os::GuestOsSecurityDelegate crostini_security_delegate("termina");
  crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1},
                                          base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("file:///mnt/chromeos/MyFiles/file1", data);

  // Crostini should join lines with CRLF.
  crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini,
                                          {file1, file2},
                                          base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ(
      "file:///mnt/chromeos/MyFiles/file1"
      "\r\n"
      "file:///mnt/chromeos/MyFiles/file2",
      data);

  // Plugin VM should convert path to inside VM.
  security_delegate.SendFileInfo(ui::EndpointType::kPluginVm, {file1},
                                 base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("file://ChromeOS/MyFiles/file1", data);

  // Bruschetta should convert path to inside VM, and share the path.
  guest_os::GuestOsSecurityDelegate bru_security_delegate("bru");
  bru_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1},
                                     base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("file:///mnt/shared/MyFiles/file1", data);

  // Crostini should handle vmfile:termina:/etc/hosts.
  file1.path = base::FilePath("vmfile:termina:/etc/hosts");
  crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1},
                                          base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("file:///etc/hosts", data);

  // Crostini should ignore vmfile:PvmDefault:C:/WINDOWS/notepad.exe.
  file1.path = base::FilePath("vmfile:PvmDefault:C:/WINDOWS/notepad.exe");
  crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1},
                                          base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("", data);

  // Plugin VM should handle vmfile:PvmDefault:C:/WINDOWS/notepad.exe.
  file1.path = base::FilePath("vmfile:PvmDefault:C:/WINDOWS/notepad.exe");
  security_delegate.SendFileInfo(ui::EndpointType::kPluginVm, {file1},
                                 base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("file:///C:/WINDOWS/notepad.exe", data);

  // Crostini should handle vmfile:termina:/etc/hosts.
  file1.path = base::FilePath("vmfile:termina:/etc/hosts");
  security_delegate.SendFileInfo(ui::EndpointType::kPluginVm, {file1},
                                 base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("", data);

  // Bruschetta should handle vmfile:bru:/etc/hosts.
  file1.path = base::FilePath("vmfile:bru:/etc/hosts");
  bru_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1},
                                     base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("file:///etc/hosts", data);

  // Bruschetta should ignore vmfile:termina:/etc/hosts.
  file1.path = base::FilePath("vmfile:termina:/etc/hosts");
  bru_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file1},
                                     base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("", data);
}

TEST_F(ChromeSecurityDelegateTest, SendFileInfoSharePathsCrostini) {
  guest_os::GuestOsSecurityDelegate crostini_security_delegate("termina");

  // A path which is already shared should not be shared again.
  base::FilePath shared_path = myfiles_dir_.Append("shared");
  auto* guest_os_share_path =
      guest_os::GuestOsSharePath::GetForProfile(profile());
  guest_os_share_path->RegisterSharedPath(crostini::kCrostiniDefaultVmName,
                                          shared_path);
  ui::FileInfo file(shared_path, base::FilePath());
  EXPECT_FALSE(FakeSeneschalClient::Get()->share_path_called());
  std::string data;
  crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file},
                                          base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("file:///mnt/chromeos/MyFiles/shared", data);
  EXPECT_FALSE(FakeSeneschalClient::Get()->share_path_called());

  // A path which is not already shared should be shared.
  file = ui::FileInfo(myfiles_dir_.Append("file"), base::FilePath());
  crostini_security_delegate.SendFileInfo(ui::EndpointType::kCrostini, {file},
                                          base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("file:///mnt/chromeos/MyFiles/file", data);
  EXPECT_TRUE(FakeSeneschalClient::Get()->share_path_called());
}

TEST_F(ChromeSecurityDelegateTest, SendFileInfoSharePathsPluginVm) {
  ChromeSecurityDelegate security_delegate;

  // Plugin VM should send empty data and not share path if not already shared.
  ui::FileInfo file(myfiles_dir_.Append("file"), base::FilePath());
  std::string data;
  security_delegate.SendFileInfo(ui::EndpointType::kPluginVm, {file},
                                 base::BindOnce(&Capture, &data));
  task_environment_.RunUntilIdle();
  EXPECT_EQ("", data);
  EXPECT_FALSE(FakeSeneschalClient::Get()->share_path_called());
}
}  // namespace ash