chromium/chrome/browser/chromeos/policy/dlp/dlp_scoped_file_access_delegate_unittest.cc

// Copyright 2022 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/chromeos/policy/dlp/dlp_scoped_file_access_delegate.h"

#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/common/chrome_paths.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/dbus/dlp/dlp_client.h"
#include "chromeos/dbus/dlp/dlp_service.pb.h"
#include "chromeos/dbus/dlp/fake_dlp_client.h"
#include "components/enterprise/data_controls/core/browser/dlp_histogram_helper.h"
#include "components/file_access/file_access_copy_or_move_delegate_factory.h"
#include "components/file_access/scoped_file_access.h"
#include "components/file_access/scoped_file_access_delegate.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace policy {

using DefaultAccess = file_access::ScopedFileAccessDelegate::DefaultAccess;

class DlpScopedFileAccessDelegateTest : public testing::Test {
 public:
  DlpScopedFileAccessDelegateTest() = default;
  ~DlpScopedFileAccessDelegateTest() override = default;

  DlpScopedFileAccessDelegateTest(const DlpScopedFileAccessDelegateTest&) =
      delete;
  DlpScopedFileAccessDelegateTest& operator=(
      const DlpScopedFileAccessDelegateTest&) = delete;

 protected:
  void InitializeWithFakeClient() {
    DlpScopedFileAccessDelegate::Initialize(base::BindLambdaForTesting(
        [this]() -> chromeos::DlpClient* { return &fake_dlp_client_; }));
  }

  content::BrowserTaskEnvironment task_environment_;
  chromeos::FakeDlpClient fake_dlp_client_;
  std::unique_ptr<DlpScopedFileAccessDelegate> delegate_{
      new DlpScopedFileAccessDelegate(base::BindLambdaForTesting(
          [this]() -> chromeos::DlpClient* { return &fake_dlp_client_; }))};
};

TEST_F(DlpScopedFileAccessDelegateTest, TestNoSingleton) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);

  base::test::TestFuture<file_access::ScopedFileAccess> future1;
  delegate_->RequestFilesAccess({file_path}, GURL("https://example.com"),
                                future1.GetCallback());
  EXPECT_TRUE(future1.Get<0>().is_allowed());

  fake_dlp_client_.SetFileAccessAllowed(false);
  base::test::TestFuture<file_access::ScopedFileAccess> future2;
  delegate_->RequestFilesAccess({file_path}, GURL("https://example.com"),
                                future2.GetCallback());
  EXPECT_FALSE(future2.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest, TestFileAccessSingletonForUrl) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);

  InitializeWithFakeClient();

  base::test::TestFuture<file_access::ScopedFileAccess> future1;
  auto* delegate = file_access::ScopedFileAccessDelegate::Get();
  delegate->RequestFilesAccess({file_path}, GURL("https://example.com"),
                               future1.GetCallback());
  EXPECT_TRUE(future1.Get<0>().is_allowed());

  fake_dlp_client_.SetFileAccessAllowed(false);
  base::test::TestFuture<file_access::ScopedFileAccess> future2;
  delegate->RequestFilesAccess({file_path}, GURL("https://example.com"),
                               future2.GetCallback());
  EXPECT_FALSE(future2.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest,
       TestFileAccessSingletonForSystemComponent) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);

  InitializeWithFakeClient();

  base::test::TestFuture<file_access::ScopedFileAccess> future1;
  auto* delegate = file_access::ScopedFileAccessDelegate::Get();
  delegate->RequestFilesAccessForSystem({file_path}, future1.GetCallback());
  EXPECT_TRUE(future1.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest, CreateFileAccessCallbackAllowTest) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);

  InitializeWithFakeClient();
  fake_dlp_client_.SetFileAccessAllowed(true);

  base::test::TestFuture<file_access::ScopedFileAccess> future;
  auto* delegate = file_access::ScopedFileAccessDelegate::Get();
  auto cb = delegate->CreateFileAccessCallback(GURL("https://google.com"));
  cb.Run({file_path}, future.GetCallback());
  EXPECT_TRUE(future.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest, CreateFileAccessCallbackDenyTest) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);

  InitializeWithFakeClient();
  fake_dlp_client_.SetFileAccessAllowed(false);

  base::test::TestFuture<file_access::ScopedFileAccess> future;
  auto* delegate = file_access::ScopedFileAccessDelegate::Get();
  auto cb = delegate->CreateFileAccessCallback(GURL("https://google.com"));
  cb.Run({file_path}, future.GetCallback());
  EXPECT_FALSE(future.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest,
       CreateFileAccessCallbackLostInstanceTest) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);

  InitializeWithFakeClient();
  fake_dlp_client_.SetFileAccessAllowed(false);

  base::test::TestFuture<file_access::ScopedFileAccess> future;
  auto* delegate = file_access::ScopedFileAccessDelegate::Get();
  auto cb = delegate->CreateFileAccessCallback(GURL("https://google.com"));
  delegate_.reset();
  cb.Run({file_path}, future.GetCallback());
  EXPECT_TRUE(future.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest, GetCallbackSystemTest) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);

  InitializeWithFakeClient();

  // Post a task on IO thread to sync with to be sure the IO task setting
  // `request_files_access_for_system_io_callback_` has run.
  base::RunLoop init;
  auto io_thread = content::GetIOThreadTaskRunner({});
  io_thread->PostTask(
      FROM_HERE, base::BindOnce(&base::RunLoop::Quit, base::Unretained(&init)));
  init.Run();

  base::test::TestFuture<file_access::ScopedFileAccess> future;
  auto* delegate = file_access::ScopedFileAccessDelegate::Get();
  auto cb = delegate->GetCallbackForSystem();
  EXPECT_TRUE(cb);
  cb.Run({file_path}, future.GetCallback());
  EXPECT_TRUE(future.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest, GetCallbackSystemNoSingletonTest) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);

  base::test::TestFuture<file_access::ScopedFileAccess> future;
  auto* delegate = file_access::ScopedFileAccessDelegate::Get();
  auto cb = delegate->GetCallbackForSystem();
  EXPECT_TRUE(cb);
  cb.Run({file_path}, future.GetCallback());
  EXPECT_TRUE(future.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest, NoDlpClientAvailable) {
  // Creating a new instance will automatically delete the old one. Reset the
  // pointer so that we don't attempt to deallocate.
  delegate_.reset();
  auto delegate =
      std::make_unique<DlpScopedFileAccessDelegate>(base::BindLambdaForTesting(
          []() -> chromeos::DlpClient* { return nullptr; }));

  // Defaults to allowed.
  base::test::TestFuture<file_access::ScopedFileAccess> future1;
  delegate->RequestFilesAccess({base::FilePath()},
                               GURL("https://no_dlp_client.com"),
                               future1.GetCallback());
  EXPECT_TRUE(future1.Get<0>().is_allowed());

  // Defaults to allowed.
  base::test::TestFuture<file_access::ScopedFileAccess> future2;
  delegate->RequestFilesAccessForSystem({base::FilePath()},
                                        future2.GetCallback());
  EXPECT_TRUE(future2.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest, DlpClientNotAlive) {
  InitializeWithFakeClient();

  fake_dlp_client_.SetIsAlive(false);

  // Defaults to allowed.
  base::test::TestFuture<file_access::ScopedFileAccess> future1;
  delegate_->RequestFilesAccess({base::FilePath()},
                                GURL("https://no_dlp_client.com"),
                                future1.GetCallback());
  EXPECT_TRUE(future1.Get<0>().is_allowed());

  // Defaults to allowed.
  base::test::TestFuture<file_access::ScopedFileAccess> future2;
  delegate_->RequestFilesAccessForSystem({base::FilePath()},
                                         future2.GetCallback());
  EXPECT_TRUE(future2.Get<0>().is_allowed());
}

TEST_F(DlpScopedFileAccessDelegateTest, TestMultipleInstances) {
  auto null_client_provider = []() -> chromeos::DlpClient* { return nullptr; };
  DlpScopedFileAccessDelegate::Initialize(
      base::BindLambdaForTesting(null_client_provider));
  EXPECT_NO_FATAL_FAILURE(DlpScopedFileAccessDelegate::Initialize(
      base::BindLambdaForTesting(null_client_provider)));
}

class DlpScopedFileAccessDelegateTaskTest : public testing::Test {
 public:
  content::BrowserTaskEnvironment browser_task_environment_{
      content::BrowserTaskEnvironment::REAL_IO_THREAD};
  base::RunLoop run_loop_;
  chromeos::FakeDlpClient fake_dlp_client_;
  scoped_refptr<base::SingleThreadTaskRunner> ui_thread_ =
      content::GetUIThreadTaskRunner({});
  scoped_refptr<base::SingleThreadTaskRunner> io_thread_ =
      content::GetIOThreadTaskRunner({});

  void SetUp() override {
    file_access::ScopedFileAccessDelegate::DeleteInstance();
    browser_task_environment_.RunUntilIdle();
  }

  void TestPreInit() {
    EXPECT_FALSE(
        file_access::FileAccessCopyOrMoveDelegateFactory::HasInstance());
    ui_thread_->PostTask(
        FROM_HERE, base::BindOnce(&DlpScopedFileAccessDelegateTaskTest::Init,
                                  base::Unretained(this)));
  }

  void Init() {
    InitializeWithFakeClient();
    io_thread_->PostTask(
        FROM_HERE,
        base::BindOnce(&DlpScopedFileAccessDelegateTaskTest::TestPostInit,
                       base::Unretained(this)));
  }

  void TestPostInit() {
    EXPECT_TRUE(
        file_access::FileAccessCopyOrMoveDelegateFactory::HasInstance());
    ui_thread_->PostTask(
        FROM_HERE, base::BindOnce(&DlpScopedFileAccessDelegateTaskTest::Delete,
                                  base::Unretained(this)));
  }

  void Delete() {
    file_access::ScopedFileAccessDelegate::DeleteInstance();
    io_thread_->PostTask(
        FROM_HERE,
        base::BindOnce(&DlpScopedFileAccessDelegateTaskTest::TestPostDelete,
                       base::Unretained(this)));
  }

  void TestPostDelete() {
    EXPECT_FALSE(
        file_access::FileAccessCopyOrMoveDelegateFactory::HasInstance());
    run_loop_.Quit();
  }

 protected:
  void InitializeWithFakeClient() {
    DlpScopedFileAccessDelegate::Initialize(base::BindLambdaForTesting(
        [this]() -> chromeos::DlpClient* { return &fake_dlp_client_; }));
  }
};

TEST_F(DlpScopedFileAccessDelegateTaskTest, TestSync) {
  io_thread_->PostTask(
      FROM_HERE,
      base::BindOnce(&DlpScopedFileAccessDelegateTaskTest::TestPreInit,
                     base::Unretained(this)));
  run_loop_.Run();
}

TEST_F(DlpScopedFileAccessDelegateTaskTest,
       TestGetDefaultFilesAccessIONoInstance) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);
  io_thread_->PostTask(
      FROM_HERE, base::BindLambdaForTesting([this, &file_path]() {
        file_access::ScopedFileAccessDelegate::RequestDefaultFilesAccessIO(
            {file_path}, base::BindLambdaForTesting(
                             [this](file_access::ScopedFileAccess file_access) {
                               DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
                               EXPECT_TRUE(file_access.is_allowed());
                               run_loop_.Quit();
                             }));
      }));
  run_loop_.Run();
}

// This test should simulate calling RequestDefaultFilesAccessIO with existing
// callback for the IO thread but destructed DlpScopedFileAccessDelegate on the
// UI thread.
TEST_F(DlpScopedFileAccessDelegateTaskTest,
       TestRequestDefaultFilesAccessIODestroyedInstance) {
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);
  InitializeWithFakeClient();
  // Dlp would disallow but missing ScopedFileAccessDelegate should fall back to
  // allow.
  fake_dlp_client_.SetFileAccessAllowed(false);

  // Post a task on IO thread to sync with to be sure the IO task setting
  // `request_files_access_for_system_io_callback_` has run.
  base::RunLoop init;
  io_thread_->PostTask(
      FROM_HERE, base::BindOnce(&base::RunLoop::Quit, base::Unretained(&init)));
  init.Run();

  // Callback that calls the original
  // request_files_access_for_system_io_callback_ after destructing
  // DlpScopedFileAccessDelegate.
  file_access::ScopedFileAccessDelegate::
      ScopedRequestFilesAccessCallbackForTesting file_access_callback(
          base::BindLambdaForTesting(
              [this, &file_access_callback](
                  const std::vector<base::FilePath>& path,
                  base::OnceCallback<void(file_access::ScopedFileAccess)>
                      callback) {
                ui_thread_->PostTask(
                    FROM_HERE,
                    base::BindOnce(&file_access::ScopedFileAccessDelegate::
                                       DeleteInstance));
                file_access_callback.RunOriginalCallback(path,
                                                         std::move(callback));
              }),
          false /* = restore_original_callback*/);
  // The request for file access should be granted as that is the default
  // behaviour for no running dlp (no rules).
  io_thread_->PostTask(
      FROM_HERE, base::BindLambdaForTesting([this, &file_path]() {
        file_access::ScopedFileAccessDelegate::RequestDefaultFilesAccessIO(
            {file_path}, base::BindLambdaForTesting(
                             [this](file_access::ScopedFileAccess file_access) {
                               DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
                               EXPECT_TRUE(file_access.is_allowed());
                               run_loop_.Quit();
                             }));
      }));
  run_loop_.Run();
}

TEST_F(DlpScopedFileAccessDelegateTaskTest, TestGetDefaultFilesAccess) {
  InitializeWithFakeClient();
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);
  base::MockRepeatingCallback<void(
      const dlp::RequestFileAccessRequest,
      chromeos::DlpClient::RequestFileAccessCallback)>
      request_file_access;
  fake_dlp_client_.SetRequestFileAccessMock(request_file_access.Get());
  dlp::RequestFileAccessResponse response;
  response.set_allowed(true);
  EXPECT_CALL(request_file_access, Run)
      .WillOnce(base::test::RunOnceCallback<1>(response, base::ScopedFD()));

  io_thread_->PostTask(
      FROM_HERE, base::BindLambdaForTesting([this, &file_path]() {
        file_access::ScopedFileAccessDelegate::RequestDefaultFilesAccessIO(
            {file_path}, base::BindLambdaForTesting(
                             [this](file_access::ScopedFileAccess file_access) {
                               DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
                               EXPECT_TRUE(file_access.is_allowed());
                               run_loop_.Quit();
                             }));
      }));
  run_loop_.Run();
}

TEST_F(DlpScopedFileAccessDelegateTaskTest, TestGetDefaultDenyFilesAccess) {
  InitializeWithFakeClient();
  base::FilePath file_path;
  base::CreateTemporaryFile(&file_path);
  base::MockRepeatingCallback<void(
      const dlp::RequestFileAccessRequest,
      chromeos::DlpClient::RequestFileAccessCallback)>
      request_file_access;
  fake_dlp_client_.SetRequestFileAccessMock(request_file_access.Get());
  EXPECT_CALL(request_file_access, Run).Times(0);

  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(
      chromeos::features::kDataControlsFileAccessDefaultDeny);

  io_thread_->PostTask(
      FROM_HERE, base::BindLambdaForTesting([this, &file_path]() {
        file_access::ScopedFileAccessDelegate::RequestDefaultFilesAccessIO(
            {file_path}, base::BindLambdaForTesting(
                             [this](file_access::ScopedFileAccess file_access) {
                               DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
                               EXPECT_TRUE(file_access.is_allowed());
                               run_loop_.Quit();
                             }));
      }));
  run_loop_.Run();
}

class DlpScopedFileAccessDelegateUMATest
    : public DlpScopedFileAccessDelegateTaskTest {
 protected:
  void RequestDefault(const base::FilePath& file_path) {
    base::RunLoop run_loop;
    io_thread_->PostTask(
        FROM_HERE, base::BindLambdaForTesting([&file_path, &run_loop]() {
          file_access::ScopedFileAccessDelegate::RequestDefaultFilesAccessIO(
              {file_path},
              base::BindLambdaForTesting(
                  [&run_loop](file_access::ScopedFileAccess file_access) {
                    run_loop.Quit();
                  }));
        }));
    run_loop.Run();
  }
  const base::HistogramTester histogram_tester_;
  base::ScopedTempDir temp_dir_;
};

// Test if the right UMA histogram is created without the default deny flag set.
TEST_F(DlpScopedFileAccessDelegateUMATest, TestUMADefaultAllow) {
  InitializeWithFakeClient();
  ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
  base::FilePath file_path = temp_dir_.GetPath();
  base::FilePath my_files = file_path.AppendASCII("MyFiles");
  ASSERT_TRUE(
      base::PathService::Override(chrome::DIR_USER_DOCUMENTS, my_files));
  RequestDefault(file_path.AppendASCII("file"));
  RequestDefault(file_path.AppendASCII("not").AppendASCII("MyFiles"));
  RequestDefault(my_files.AppendASCII("file"));
  EXPECT_THAT(
      histogram_tester_.GetAllSamples(
          data_controls::GetDlpHistogramPrefix() +
          std::string(data_controls::dlp::kFilesDefaultFileAccess)),
      base::BucketsAre(base::Bucket(DefaultAccess::kMyFilesAllow, 1),
                       base::Bucket(DefaultAccess::kSystemFilesAllow, 2),
                       base::Bucket(DefaultAccess::kMyFilesDeny, 0),
                       base::Bucket(DefaultAccess::kSystemFilesDeny, 0)));
}

// Test if the right UMA histogram is created with the default deny flag set.
TEST_F(DlpScopedFileAccessDelegateUMATest, TestUMADefaultDeny) {
  InitializeWithFakeClient();
  ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
  base::FilePath file_path = temp_dir_.GetPath();
  base::FilePath my_files = file_path.AppendASCII("MyFiles");
  ASSERT_TRUE(
      base::PathService::Override(chrome::DIR_USER_DOCUMENTS, my_files));
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(
      chromeos::features::kDataControlsFileAccessDefaultDeny);
  RequestDefault(file_path.AppendASCII("file"));
  RequestDefault(file_path.AppendASCII("not").AppendASCII("MyFiles"));
  RequestDefault(my_files.AppendASCII("file"));
  EXPECT_THAT(
      histogram_tester_.GetAllSamples(
          data_controls::GetDlpHistogramPrefix() +
          std::string(data_controls::dlp::kFilesDefaultFileAccess)),
      base::BucketsAre(base::Bucket(DefaultAccess::kMyFilesAllow, 0),
                       base::Bucket(DefaultAccess::kSystemFilesAllow, 0),
                       base::Bucket(DefaultAccess::kMyFilesDeny, 1),
                       base::Bucket(DefaultAccess::kSystemFilesDeny, 2)));
}

}  // namespace policy