// 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 "chrome/browser/chromeos/policy/dlp/dlp_files_controller.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "base/files/file_path.h"
#include "base/path_service.h"
#include "base/process/process_handle.h"
#include "base/task/bind_post_task.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_files_utils.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_scoped_file_access_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chromeos/dbus/dlp/dlp_client.h"
#include "chromeos/dbus/dlp/dlp_service.pb.h"
#include "components/enterprise/data_controls/core/browser/component.h"
#include "components/file_access/scoped_file_access_copy.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "storage/browser/file_system/file_system_url.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
namespace policy {
namespace {
// FileSystemContext instance set for testing.
std::optional<storage::FileSystemContext*> g_file_system_context_for_testing =
std::nullopt;
// This callback is used when we copy a file within the internal filesystem
// (Downloads / MyFiles). It is called after the source URL of the source file
// is retrieved. It creates a callback `delayed_add_file` and requests the
// ScopedFileAccess for the copy operation. To this access token the
// `delayed_add_file` callback is added so it is called after the copy operation
// finishes.
void GotFilesSourcesOfCopy(
storage::FileSystemURL destination,
::dlp::RequestFileAccessRequest file_access_request,
base::OnceCallback<void(std::unique_ptr<file_access::ScopedFileAccess>)>
result_callback,
const ::dlp::GetFilesSourcesResponse response) {
if (response.files_metadata_size() == 0) {
std::move(result_callback)
.Run(std::make_unique<file_access::ScopedFileAccess>(
file_access::ScopedFileAccess::Allowed()));
return;
}
DCHECK(response.files_metadata_size() == 1);
if (!chromeos::DlpClient::Get() || !chromeos::DlpClient::Get()->IsAlive()) {
std::move(result_callback)
.Run(std::make_unique<file_access::ScopedFileAccess>(
file_access::ScopedFileAccess::Allowed()));
return;
}
if (!response.files_metadata().Get(0).has_source_url() ||
response.files_metadata().Get(0).source_url().empty()) {
std::move(result_callback)
.Run(std::make_unique<file_access::ScopedFileAccess>(
file_access::ScopedFileAccess::Allowed()));
return;
}
::dlp::AddFilesRequest request;
::dlp::AddFileRequest* add_request = request.add_add_file_requests();
add_request->set_file_path(destination.path().value());
add_request->set_source_url(response.files_metadata().Get(0).source_url());
add_request->set_referrer_url(
response.files_metadata().Get(0).referrer_url());
// The callback will be invoked with the destruction of the
// ScopedFileAccessCopy object
base::OnceCallback<void()> delayed_add_file = base::BindPostTask(
base::SingleThreadTaskRunner::GetCurrentDefault(),
base::BindOnce(
[](::dlp::AddFilesRequest&& request) {
// TODO(https://crbug.com/1368497): we might want to use the
// callback for error handling.
chromeos::DlpClient::Get()->AddFiles(request, base::DoNothing());
},
std::move(request)));
chromeos::DlpClient::RequestFileAccessCallback add_file_callback =
base::BindOnce(
[](base::OnceCallback<void(
std::unique_ptr<file_access::ScopedFileAccess>)>
result_callback,
base::OnceCallback<void()> delayed_add_file,
const ::dlp::RequestFileAccessResponse response,
base::ScopedFD fd) {
std::move(result_callback)
.Run(std::make_unique<file_access::ScopedFileAccessCopy>(
response.allowed(), std::move(fd),
std::move(delayed_add_file)));
},
std::move(result_callback), std::move(delayed_add_file));
chromeos::DlpClient::Get()->RequestFileAccess(file_access_request,
std::move(add_file_callback));
}
// Converts DataTransferEndpoint object to DlpFileDestination.
DlpFileDestination DTEndpointToFileDestination(
const ui::DataTransferEndpoint* endpoint) {
DCHECK(endpoint);
switch (endpoint->type()) {
case ui::EndpointType::kUrl:
DCHECK(endpoint->GetURL());
return DlpFileDestination(*endpoint->GetURL());
case ui::EndpointType::kArc:
return DlpFileDestination(data_controls::Component::kArc);
case ui::EndpointType::kCrostini:
return DlpFileDestination(data_controls::Component::kCrostini);
case ui::EndpointType::kPluginVm:
return DlpFileDestination(data_controls::Component::kPluginVm);
case ui::EndpointType::kLacros:
case ui::EndpointType::kDefault:
case ui::EndpointType::kClipboardHistory:
case ui::EndpointType::kBorealis:
case ui::EndpointType::kUnknownVm:
return DlpFileDestination(data_controls::Component::kUnknownComponent);
}
}
// Converts file paths to file system URLs.
std::vector<storage::FileSystemURL> ConvertLocalFilePathsToFileSystemUrls(
const storage::FileSystemContext& file_system_context,
const std::vector<base::FilePath>& paths) {
std::vector<storage::FileSystemURL> file_system_urls;
for (const auto& path : paths) {
file_system_urls.push_back(file_system_context.CreateCrackedFileSystemURL(
blink::StorageKey(), storage::kFileSystemTypeLocal, path));
}
return file_system_urls;
}
} // namespace
DlpFilesController::FileDaemonInfo::FileDaemonInfo(
ino64_t inode,
time_t crtime,
const base::FilePath& path,
const std::string& source_url,
const std::string& referrer_url)
: inode(inode),
crtime(crtime),
path(path),
source_url(source_url),
referrer_url(referrer_url) {}
DlpFilesController::FileDaemonInfo::FileDaemonInfo(const FileDaemonInfo& o)
: inode(o.inode),
crtime(o.crtime),
path(o.path),
source_url(o.source_url),
referrer_url(o.referrer_url) {}
DlpFilesController::FolderRecursionDelegate::FolderRecursionDelegate(
storage::FileSystemContext* file_system_context,
const storage::FileSystemURL& root,
FileURLsCallback callback)
: RecursiveOperationDelegate(file_system_context),
root_(root),
callback_(std::move(callback)) {}
DlpFilesController::FolderRecursionDelegate::~FolderRecursionDelegate() =
default;
void DlpFilesController::FolderRecursionDelegate::Run() {
NOTREACHED_IN_MIGRATION();
}
void DlpFilesController::FolderRecursionDelegate::RunRecursively() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
StartRecursiveOperation(*root_,
storage::FileSystemOperation::ERROR_BEHAVIOR_SKIP,
base::BindOnce(&FolderRecursionDelegate::Completed,
weak_ptr_factory_.GetWeakPtr()));
}
void DlpFilesController::FolderRecursionDelegate::ProcessFile(
const storage::FileSystemURL& url,
StatusCallback callback) {
file_system_context()->operation_runner()->GetMetadata(
url, {storage::FileSystemOperation::GetMetadataField::kIsDirectory},
base::BindOnce(&FolderRecursionDelegate::OnGetMetadata,
weak_ptr_factory_.GetWeakPtr(), url, std::move(callback)));
}
void DlpFilesController::FolderRecursionDelegate::ProcessDirectory(
const storage::FileSystemURL& url,
StatusCallback callback) {
std::move(callback).Run(base::File::FILE_OK);
}
void DlpFilesController::FolderRecursionDelegate::PostProcessDirectory(
const storage::FileSystemURL& url,
StatusCallback callback) {
std::move(callback).Run(base::File::FILE_OK);
}
base::WeakPtr<storage::RecursiveOperationDelegate>
DlpFilesController::FolderRecursionDelegate::AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void DlpFilesController::FolderRecursionDelegate::OnGetMetadata(
const storage::FileSystemURL& url,
StatusCallback callback,
base::File::Error result,
const base::File::Info& file_info) {
if (result != base::File::FILE_OK) {
std::move(callback).Run(result);
return;
}
if (file_info.is_directory) {
std::move(callback).Run(base::File::FILE_ERROR_NOT_A_FILE);
return;
}
files_urls_.push_back(url);
std::move(callback).Run(base::File::FILE_OK);
}
void DlpFilesController::FolderRecursionDelegate::Completed(
base::File::Error result) {
std::move(callback_).Run(std::move(files_urls_));
}
DlpFilesController::RootsRecursionDelegate::RootsRecursionDelegate(
storage::FileSystemContext* file_system_context,
std::vector<storage::FileSystemURL> roots,
DlpFilesController::FolderRecursionDelegate::FileURLsCallback callback)
: file_system_context_(file_system_context),
roots_(std::move(roots)),
callback_(std::move(callback)) {}
DlpFilesController::RootsRecursionDelegate::~RootsRecursionDelegate() = default;
void DlpFilesController::RootsRecursionDelegate::Run() {
for (const auto& root : roots_) {
auto recursion_delegate = std::make_unique<FolderRecursionDelegate>(
file_system_context_, root,
base::BindOnce(&RootsRecursionDelegate::Completed,
weak_ptr_factory_.GetWeakPtr()));
recursion_delegate->RunRecursively();
delegates_.push_back(std::move(recursion_delegate));
}
}
void DlpFilesController::RootsRecursionDelegate::Completed(
std::vector<storage::FileSystemURL> files_urls) {
counter_++;
files_urls_.insert(std::end(files_urls_), std::begin(files_urls),
std::end(files_urls));
if (counter_ == roots_.size()) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback_), std::move(files_urls_)));
content::GetIOThreadTaskRunner({})->DeleteSoon(FROM_HERE, this);
}
}
DlpFilesController::DlpFilesController(const DlpRulesManager& rules_manager)
: rules_manager_(rules_manager) {}
DlpFilesController::~DlpFilesController() = default;
void DlpFilesController::RequestCopyAccess(
const storage::FileSystemURL& source_file,
const storage::FileSystemURL& destination,
base::OnceCallback<void(std::unique_ptr<file_access::ScopedFileAccess>)>
result_callback) {
if (!chromeos::DlpClient::Get() || !chromeos::DlpClient::Get()->IsAlive()) {
std::move(result_callback)
.Run(std::make_unique<file_access::ScopedFileAccess>(
file_access::ScopedFileAccess::Allowed()));
return;
}
Profile* profile = ProfileManager::GetPrimaryUserProfile();
std::optional<data_controls::Component> dst_component =
MapFilePathToPolicyComponent(profile, destination.path());
std::optional<data_controls::Component> src_component =
MapFilePathToPolicyComponent(profile, source_file.path());
// Copy from external is not limited by DLP.
// TODO(b/297190245): currently there is no component for mounted archives and
// they are considered as not in the local file system so we end up in the if
// below when a file is copied from a mounted archive. When mounting of
// restricted archives is supported, we however need to apply the restriction
// of the source archive to the copied files and not just always allow as
// below.
if (src_component.has_value() || !IsInLocalFileSystem(source_file.path())) {
std::move(result_callback)
.Run(std::make_unique<file_access::ScopedFileAccess>(
file_access::ScopedFileAccess::Allowed()));
return;
}
::dlp::DlpComponent proto =
dst_component ? dlp::MapPolicyComponentToProto(*dst_component)
: ::dlp::DlpComponent::SYSTEM;
::dlp::RequestFileAccessRequest file_access_request;
file_access_request.set_process_id(base::GetCurrentProcId());
file_access_request.add_files_paths(source_file.path().value());
file_access_request.set_destination_component(proto);
if (!dst_component.has_value()) {
// We allow internal copy, we still have to get the scopedFS
// and we might need to copy the source URL information.
if (IsInLocalFileSystem(destination.path())) {
::dlp::GetFilesSourcesRequest request;
request.add_files_paths(source_file.path().value());
chromeos::DlpClient::Get()->GetFilesSources(
request,
base::BindOnce(&GotFilesSourcesOfCopy, destination,
file_access_request, std::move(result_callback)));
} else {
std::move(result_callback)
.Run(std::make_unique<file_access::ScopedFileAccess>(
/*allowed=*/false, base::ScopedFD()));
}
return;
}
chromeos::DlpClient::Get()->RequestFileAccess(
file_access_request,
base::BindOnce(
[](base::OnceCallback<void(
std::unique_ptr<file_access::ScopedFileAccess>)> callback,
::dlp::RequestFileAccessResponse res, base::ScopedFD fd) {
std::move(callback).Run(
std::make_unique<file_access::ScopedFileAccess>(res.allowed(),
std::move(fd)));
},
std::move(result_callback)));
}
void DlpFilesController::CheckIfPasteOrDropIsAllowed(
const std::vector<base::FilePath>& files,
const ui::DataTransferEndpoint* data_dst,
CheckIfDlpAllowedCallback result_callback) {
std::vector<base::FilePath> local_files;
for (const auto& path : files) {
if (!IsInLocalFileSystem(path)) {
continue;
}
local_files.push_back(path);
}
scoped_refptr<storage::FileSystemContext> file_system_context =
GetFileSystemContextForPrimaryProfile();
if (!file_system_context) {
std::move(result_callback).Run(/*is_allowed=*/true);
return;
}
std::vector<storage::FileSystemURL> files_urls =
ConvertLocalFilePathsToFileSystemUrls(*file_system_context, local_files);
if (files_urls.empty()) {
std::move(result_callback).Run(/*is_allowed=*/true);
return;
}
DlpFileDestination destination = DTEndpointToFileDestination(data_dst);
auto* roots_recursion_delegate = new RootsRecursionDelegate(
file_system_context.get(), std::move(files_urls),
base::BindOnce(&DlpFilesController::ContinueCheckIfPasteOrDropIsAllowed,
weak_ptr_factory_.GetWeakPtr(), std::move(destination),
std::move(result_callback)));
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&RootsRecursionDelegate::Run,
// base::Unretained() is safe since |recursion_delegate|
// will delete itself after all the files list if ready.
base::Unretained(roots_recursion_delegate)));
}
storage::FileSystemContext*
DlpFilesController::GetFileSystemContextForPrimaryProfile() {
if (g_file_system_context_for_testing.has_value()) {
return g_file_system_context_for_testing.value();
}
Profile* profile = ProfileManager::GetPrimaryUserProfile();
content::StoragePartition* storage = profile->GetDefaultStoragePartition();
return storage->GetFileSystemContext();
}
void DlpFilesController::SetFileSystemContextForTesting(
storage::FileSystemContext* file_system_context) {
g_file_system_context_for_testing = file_system_context;
}
void DlpFilesController::ContinueCheckIfPasteOrDropIsAllowed(
const DlpFileDestination& destination,
CheckIfDlpAllowedCallback result_callback,
std::vector<storage::FileSystemURL> files) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!chromeos::DlpClient::Get() || !chromeos::DlpClient::Get()->IsAlive()) {
std::move(result_callback).Run(/*is_allowed=*/true);
return;
}
::dlp::CheckFilesTransferRequest request;
for (const auto& file : files) {
request.add_files_paths(file.path().value());
}
if (destination.component().has_value()) {
request.set_destination_component(
dlp::MapPolicyComponentToProto(destination.component().value()));
} else {
DCHECK(destination.url());
request.set_destination_url(destination.url()->spec());
}
request.set_file_action(::dlp::FileAction::COPY);
auto return_drop_allowed_cb =
base::BindOnce(&DlpFilesController::ReturnIfActionAllowed,
weak_ptr_factory_.GetWeakPtr(), dlp::FileAction::kCopy,
std::move(result_callback));
chromeos::DlpClient::Get()->CheckFilesTransfer(
request, std::move(return_drop_allowed_cb));
}
void DlpFilesController::ReturnIfActionAllowed(
dlp::FileAction action,
CheckIfDlpAllowedCallback result_callback,
::dlp::CheckFilesTransferResponse response) {
if (response.has_error_message()) {
LOG(ERROR) << "Failed to get check files transfer, error: "
<< response.error_message();
std::move(result_callback).Run(/*is_allowed=*/true);
return;
}
if (response.files_paths().empty()) {
std::move(result_callback).Run(/*is_allowed=*/true);
return;
}
std::vector<base::FilePath> blocked_files(response.files_paths().begin(),
response.files_paths().end());
ShowDlpBlockedFiles(/*task_id=*/std::nullopt, std::move(blocked_files),
action);
std::move(result_callback).Run(/*is_allowed=*/false);
}
} // namespace policy