blob: 178261c886f12e1e3049bd97a3f748d3ecf11fdb [file] [log] [blame]
// 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/enterprise/data_protection/paste_allowed_request.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "chrome/browser/enterprise/connectors/analysis/clipboard_request_handler.h"
#include "chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.h"
#include "chrome/browser/enterprise/connectors/test/deep_scanning_test_utils.h"
#include "chrome/browser/enterprise/connectors/test/fake_clipboard_request_handler.h"
#include "chrome/browser/enterprise/connectors/test/fake_content_analysis_delegate.h"
#include "chrome/browser/enterprise/data_protection/data_protection_clipboard_utils.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/enterprise/connectors/core/cloud_content_scanning/common.h"
#include "components/enterprise/connectors/core/content_analysis_delegate_base.h"
#include "components/enterprise/data_controls/core/browser/test_utils.h"
#include "content/public/browser/clipboard_types.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/clipboard/clipboard_monitor.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/clipboard/test/test_clipboard.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/services/network_config/public/cpp/cros_network_config_test_helper.h"
#endif
namespace enterprise_data_protection {
namespace {
constexpr char kScanId[] = "scan_id";
enterprise_connectors::ContentAnalysisResponse::Result CreateResult(
enterprise_connectors::ContentAnalysisResponse::Result::TriggeredRule::
Action action) {
enterprise_connectors::ContentAnalysisResponse::Result result;
result.set_tag("dlp");
result.set_status(
enterprise_connectors::ContentAnalysisResponse::Result::SUCCESS);
if (action != enterprise_connectors::ContentAnalysisResponse::Result::
TriggeredRule::ACTION_UNSPECIFIED) {
auto* rule = result.add_triggered_rules();
rule->set_rule_name("paste_rule_name");
rule->set_action(action);
}
return result;
}
enterprise_connectors::ContentAnalysisResponse CreateResponse(
enterprise_connectors::ContentAnalysisResponse::Result::TriggeredRule::
Action action) {
enterprise_connectors::ContentAnalysisResponse response;
response.set_request_token(kScanId);
auto* result = response.add_results();
*result = CreateResult(action);
return response;
}
class TestClipboardRequestHandler
: public enterprise_connectors::test::FakeClipboardRequestHandler {
public:
static std::unique_ptr<ClipboardRequestHandler> Create(
enterprise_connectors::ContentAnalysisInfo* content_analysis_info,
safe_browsing::BinaryUploadService* upload_service,
Profile* profile,
GURL url,
Type type,
enterprise_connectors::DeepScanAccessPoint access_point,
enterprise_connectors::ContentMetaData::CopiedTextSource clipboard_source,
std::string source_content_area_email,
std::string content_transfer_method,
std::string data,
CompletionCallback callback) {
return base::WrapUnique(new TestClipboardRequestHandler(
content_analysis_info, upload_service, profile, std::move(url), type,
access_point, std::move(clipboard_source),
std::move(source_content_area_email),
std::move(content_transfer_method), std::move(data),
std::move(callback)));
}
protected:
using FakeClipboardRequestHandler::FakeClipboardRequestHandler;
private:
void UploadForDeepScanning(
std::unique_ptr<enterprise_connectors::ClipboardAnalysisRequest> request)
override {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&TestClipboardRequestHandler::OnContentAnalysisResponse,
base::Unretained(this),
enterprise_connectors::ScanRequestUploadResult::SUCCESS,
CreateResponse(enterprise_connectors::ContentAnalysisResponse::
Result::TriggeredRule::BLOCK)));
}
};
class PasteAllowedRequestTest : public testing::Test {
public:
PasteAllowedRequestTest()
: profile_manager_(TestingBrowserProcess::GetGlobal()) {
EXPECT_TRUE(profile_manager_.SetUp());
profile_ = profile_manager_.CreateTestingProfile("test-user-1");
}
void SetUp() override {
PasteAllowedRequest::CleanupRequestsForTesting();
ui::TestClipboard::CreateForCurrentThread();
#if BUILDFLAG(IS_CHROMEOS)
network_config_helper_ =
std::make_unique<ash::network_config::CrosNetworkConfigTestHelper>();
ash::NetworkHandler::Initialize();
#endif
}
void TearDown() override {
#if BUILDFLAG(IS_CHROMEOS)
ash::NetworkHandler::Shutdown();
#endif
}
content::WebContents* main_web_contents() {
if (!main_web_contents_) {
content::WebContents::CreateParams params(profile_);
main_web_contents_ = content::WebContents::Create(params);
}
return main_web_contents_.get();
}
content::RenderFrameHost& main_rfh() {
return *main_web_contents()->GetPrimaryMainFrame();
}
content::ClipboardEndpoint main_endpoint() {
return content::ClipboardEndpoint(
ui::DataTransferEndpoint(GURL("https://google.com")),
base::BindLambdaForTesting([this]() -> content::BrowserContext* {
return static_cast<content::BrowserContext*>(profile_);
}),
main_rfh());
}
content::WebContents* secondary_web_contents() {
if (!secondary_web_contents_) {
content::WebContents::CreateParams params(profile_);
secondary_web_contents_ = content::WebContents::Create(params);
}
return secondary_web_contents_.get();
}
content::RenderFrameHost& secondary_rfh() {
return *secondary_web_contents()->GetPrimaryMainFrame();
}
content::ClipboardEndpoint secondary_endpoint() {
return content::ClipboardEndpoint(
ui::DataTransferEndpoint(GURL("https://google.com")),
base::BindLambdaForTesting([this]() -> content::BrowserContext* {
return static_cast<content::BrowserContext*>(profile_);
}),
secondary_rfh());
}
protected:
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
TestingProfileManager profile_manager_;
raw_ptr<TestingProfile> profile_;
std::unique_ptr<content::WebContents> main_web_contents_;
std::unique_ptr<content::WebContents> secondary_web_contents_;
#if BUILDFLAG(IS_CHROMEOS)
std::unique_ptr<ash::network_config::CrosNetworkConfigTestHelper>
network_config_helper_;
#endif
};
class PasteAllowedRequestScanningTest : public PasteAllowedRequestTest {
public:
PasteAllowedRequestScanningTest() {
enterprise_connectors::ContentAnalysisDelegate::DisableUIForTesting();
}
void SetUp() override {
PasteAllowedRequestTest::SetUp();
helper_ = std::make_unique<
enterprise_connectors::test::EventReportValidatorHelper>(profile_);
enterprise_connectors::test::SetAnalysisConnector(
profile_->GetPrefs(), enterprise_connectors::BULK_DATA_ENTRY,
R"({
"service_provider": "google",
"block_until_verdict": 1,
"minimum_data_size": 1,
"enable": [
{
"url_list": ["*"],
"tags": ["dlp"]
}
]
})");
}
protected:
std::unique_ptr<enterprise_connectors::test::EventReportValidatorHelper>
helper_;
};
} // namespace
TEST_F(PasteAllowedRequestTest, AddCallbacksAndComplete) {
PasteAllowedRequest request;
content::ClipboardPasteData clipboard_paste_data_1;
clipboard_paste_data_1.text = u"text";
clipboard_paste_data_1.png = {1, 2, 3, 4, 5};
content::ClipboardPasteData clipboard_paste_data_2;
clipboard_paste_data_2.text = u"other text";
clipboard_paste_data_2.png = {6, 7, 8, 9, 10};
int count = 0;
// Add a callback. It should not fire right away.
request.AddCallback(base::BindLambdaForTesting(
[&count, clipboard_paste_data_1](
std::optional<content::ClipboardPasteData> clipboard_paste_data) {
++count;
ASSERT_EQ(clipboard_paste_data->text, clipboard_paste_data_1.text);
ASSERT_EQ(clipboard_paste_data->png, clipboard_paste_data_1.png);
}));
EXPECT_EQ(0, count);
// Complete the request. Callback should fire. Whether paste is allowed
// or not is not important.
request.Complete(clipboard_paste_data_1);
EXPECT_EQ(1, count);
// Add a second callback. It should not fire right away.
request.AddCallback(base::BindLambdaForTesting(
[&count, clipboard_paste_data_2](
std::optional<content::ClipboardPasteData> clipboard_paste_data) {
++count;
ASSERT_EQ(clipboard_paste_data->text, clipboard_paste_data_2.text);
ASSERT_EQ(clipboard_paste_data->png, clipboard_paste_data_2.png);
}));
EXPECT_EQ(1, count);
// Calling `Complete()` again will call the second callback.
request.Complete(clipboard_paste_data_2);
EXPECT_EQ(2, count);
}
TEST_F(PasteAllowedRequestTest, IsObsolete) {
PasteAllowedRequest request;
content::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = u"data";
// A request is obsolete once it is too old and completed.
// Whether paste is allowed or not is not important.
request.Complete(clipboard_paste_data);
EXPECT_TRUE(
request.IsObsolete(request.completed_time() +
PasteAllowedRequest::kIsPasteAllowedRequestTooOld +
base::Microseconds(1)));
}
TEST_F(PasteAllowedRequestTest, SameDestinationSource) {
auto seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste);
const std::u16string kText = u"text";
content::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = kText;
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ main_endpoint(), /*destination*/ main_endpoint(),
{.seqno = seqno}, clipboard_paste_data, future.GetCallback());
ASSERT_TRUE(future.Get());
ASSERT_EQ(future.Get()->text, kText);
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest, SameDestinationSource_AfterReplacement) {
data_controls::SetDataControls(profile_->GetPrefs(), {
R"({
"sources": {
"urls": ["google.com"]
},
"destinations": {
"os_clipboard": true
},
"restrictions": [
{"class": "CLIPBOARD", "level": "BLOCK"}
]
})"});
const std::u16string kText = u"text";
content::ClipboardPasteData copied_data;
copied_data.text = kText;
base::test::TestFuture<const ui::ClipboardFormatType&,
const content::ClipboardPasteData&,
std::optional<std::u16string>>
copy_future;
IsClipboardCopyAllowedByPolicy(main_endpoint(), {}, copied_data,
copy_future.GetCallback());
auto replacement = copy_future.Get<std::optional<std::u16string>>();
EXPECT_TRUE(replacement);
EXPECT_EQ(*replacement,
u"Pasting this content here is blocked by your administrator.");
// This triggers the clipboard observer started by the
// `IsClipboardCopyAllowedByPolicy` calls so that they're aware of the new
// seqno.
ui::ClipboardMonitor::GetInstance()->NotifyClipboardDataChanged();
auto seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste);
// After the data was replaced when initially copied, it should be put back
// when pasting in the same tab.
content::ClipboardPasteData pasted_data;
pasted_data.html = u"to be replaced";
base::test::TestFuture<std::optional<content::ClipboardPasteData>>
paste_future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ main_endpoint(), /*destination*/ main_endpoint(),
{.seqno = seqno}, pasted_data, paste_future.GetCallback());
ASSERT_TRUE(paste_future.Get());
ASSERT_EQ(paste_future.Get()->text, kText);
ASSERT_TRUE(paste_future.Get()->html.empty());
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest, DifferentDestinationSource) {
auto seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste);
const std::u16string kText = u"text";
content::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = kText;
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ secondary_endpoint(), /*destination*/ main_endpoint(),
{.seqno = seqno}, clipboard_paste_data, future.GetCallback());
ASSERT_TRUE(future.Get());
ASSERT_EQ(future.Get()->text, kText);
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest,
DifferentDestinationSource_AllowedWithCachedRequest) {
const std::u16string kText = u"text";
auto seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste);
content::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = kText;
PasteAllowedRequest request;
request.Complete(clipboard_paste_data);
PasteAllowedRequest::AddRequestToCacheForTesting(main_rfh().GetGlobalId(),
seqno, std::move(request));
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
// Attempting to start a new request when there is an allowed cached request
// with the same seqno should allow again, but not by making a new request.
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ secondary_endpoint(), /*destination*/ main_endpoint(),
{.seqno = seqno}, clipboard_paste_data, future.GetCallback());
ASSERT_TRUE(future.Wait());
ASSERT_TRUE(future.Get());
ASSERT_EQ(future.Get()->text, kText);
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest,
DifferentDestinationSource_BlockedWithCachedRequest) {
auto seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste);
PasteAllowedRequest request;
request.Complete(std::nullopt);
PasteAllowedRequest::AddRequestToCacheForTesting(main_rfh().GetGlobalId(),
seqno, std::move(request));
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
const std::u16string kText = u"text";
content::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = kText;
// Attempting to start a new request when there is a blocked cached request
// with the same seqno should block again, but not by making a new request.
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ secondary_endpoint(), /*destination*/ main_endpoint(),
{.seqno = seqno}, clipboard_paste_data, future.GetCallback());
ASSERT_TRUE(future.Wait());
ASSERT_FALSE(future.Get());
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest, UnknownSource) {
auto seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste);
const std::u16string kText = u"text";
content::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = kText;
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ content::ClipboardEndpoint(std::nullopt),
/*destination*/ main_endpoint(), {.seqno = seqno}, clipboard_paste_data,
future.GetCallback());
ASSERT_TRUE(future.Get());
ASSERT_EQ(future.Get()->text, kText);
// When a document reads from the clipboard, but the clipboard was written
// from an unknown source, content checks should not be skipped.
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest, EmptyData) {
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ secondary_endpoint(),
/*destination*/ main_endpoint(),
{
.seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste),
},
content::ClipboardPasteData(), future.GetCallback());
ASSERT_TRUE(future.Get());
ASSERT_TRUE(future.Get()->empty());
// When data is empty, a request is still made in case the data needs to be
// replaced later.
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest, EmptyData_SameSourceReplaced) {
data_controls::SetDataControls(profile_->GetPrefs(), {
R"({
"destinations": {
"os_clipboard": true
},
"restrictions": [
{"class": "CLIPBOARD", "level": "BLOCK"}
]
})"});
const std::u16string kText = u"text";
content::ClipboardPasteData copied_data;
copied_data.text = kText;
base::test::TestFuture<const ui::ClipboardFormatType&,
const content::ClipboardPasteData&,
std::optional<std::u16string>>
copy_future;
IsClipboardCopyAllowedByPolicy(main_endpoint(), {}, copied_data,
copy_future.GetCallback());
auto replacement = copy_future.Get<std::optional<std::u16string>>();
EXPECT_TRUE(replacement);
EXPECT_EQ(*replacement,
u"Pasting this content here is blocked by your administrator.");
// This triggers the clipboard observer started by the
// `IsClipboardCopyAllowedByPolicy` calls so that they're aware of the new
// seqno.
ui::ClipboardMonitor::GetInstance()->NotifyClipboardDataChanged();
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ secondary_endpoint(),
/*destination*/ main_endpoint(),
{
.seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste),
},
content::ClipboardPasteData(), future.GetCallback());
ASSERT_TRUE(future.Get());
ASSERT_EQ(future.Get()->text, kText);
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest, EmptyData_DifferentSourceReplaced) {
data_controls::SetDataControls(profile_->GetPrefs(), {
R"({
"destinations": {
"os_clipboard": true
},
"restrictions": [
{"class": "CLIPBOARD", "level": "BLOCK"}
]
})"});
const std::u16string kText = u"text";
content::ClipboardPasteData copied_data;
copied_data.text = kText;
base::test::TestFuture<const ui::ClipboardFormatType&,
const content::ClipboardPasteData&,
std::optional<std::u16string>>
copy_future;
IsClipboardCopyAllowedByPolicy(main_endpoint(), {}, copied_data,
copy_future.GetCallback());
auto replacement = copy_future.Get<std::optional<std::u16string>>();
EXPECT_TRUE(replacement);
EXPECT_EQ(*replacement,
u"Pasting this content here is blocked by your administrator.");
// This triggers the clipboard observer started by the
// `IsClipboardCopyAllowedByPolicy` calls so that they're aware of the new
// seqno.
ui::ClipboardMonitor::GetInstance()->NotifyClipboardDataChanged();
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ main_endpoint(),
/*destination*/ secondary_endpoint(),
{
.seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste),
},
content::ClipboardPasteData(), future.GetCallback());
ASSERT_TRUE(future.Get());
ASSERT_EQ(future.Get()->text, kText);
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestTest, CleanupObsoleteScanRequests) {
auto seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste);
const std::u16string kText = u"text";
content::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = kText;
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ secondary_endpoint(), /*destination*/ main_endpoint(),
{.seqno = seqno}, clipboard_paste_data, future.GetCallback());
ASSERT_TRUE(future.Get());
ASSERT_EQ(future.Get()->text, kText);
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
// Make sure an appropriate amount of time passes to make the request old.
// It should be cleaned up.
task_environment_.FastForwardBy(
PasteAllowedRequest::kIsPasteAllowedRequestTooOld +
base::Microseconds(1));
PasteAllowedRequest::CleanupObsoleteRequests();
EXPECT_EQ(0u, PasteAllowedRequest::requests_count_for_testing());
}
TEST_F(PasteAllowedRequestScanningTest, DifferentDestinationSource) {
enterprise_connectors::ClipboardRequestHandler::SetFactoryForTesting(
base::BindRepeating(TestClipboardRequestHandler::Create));
auto validator = helper_->CreateValidator();
base::RunLoop run_loop;
validator.SetDoneClosure(run_loop.QuitClosure());
validator.ExpectSensitiveDataEvent(
/*url*/
"",
/*tab_url*/ "",
/*source*/ "https://google.com/",
/*destination*/ "",
/*filename*/ "Text data",
/*sha*/ "",
/*trigger*/ "WEB_CONTENT_UPLOAD",
/*dlp_verdict*/
CreateResult(enterprise_connectors::ContentAnalysisResponse::Result::
TriggeredRule::BLOCK),
/*mimetype*/
[]() {
static std::set<std::string> set = {"text/plain"};
return &set;
}(),
/*size*/ 4,
/*result*/
enterprise_connectors::EventResultToString(
enterprise_connectors::EventResult::BLOCKED),
/*username*/ "test-user@chromium.org",
/*profile_identifier*/ profile_->GetPath().AsUTF8Unsafe(),
/*scan_id*/ kScanId,
/*content_transfer_method*/ std::nullopt,
/*user_justification*/ std::nullopt);
auto seqno = ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
ui::ClipboardBuffer::kCopyPaste);
const std::u16string kText = u"text";
content::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = kText;
base::test::TestFuture<std::optional<content::ClipboardPasteData>> future;
PasteAllowedRequest::StartPasteAllowedRequest(
/*source*/ secondary_endpoint(), /*destination*/ main_endpoint(),
{.seqno = seqno}, clipboard_paste_data, future.GetCallback());
ASSERT_TRUE(future.Wait());
ASSERT_FALSE(future.Get().has_value());
EXPECT_EQ(1u, PasteAllowedRequest::requests_count_for_testing());
run_loop.Run();
}
} // namespace enterprise_data_protection