chromium/chrome/browser/ash/file_manager/copy_or_move_io_task_policy_impl.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/ash/file_manager/copy_or_move_io_task_policy_impl.h"

#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/check_op.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/ash/file_manager/copy_or_move_io_task_impl.h"
#include "chrome/browser/ash/file_manager/file_manager_copy_or_move_hook_delegate.h"
#include "chrome/browser/ash/file_manager/file_manager_copy_or_move_hook_file_check_delegate.h"
#include "chrome/browser/ash/file_manager/io_task.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/policy/dlp/dialogs/files_policy_dialog.h"
#include "chrome/browser/ash/policy/dlp/dialogs/files_policy_dialog_utils.h"
#include "chrome/browser/ash/policy/dlp/dlp_files_controller_ash.h"
#include "chrome/browser/ash/policy/dlp/files_policy_notification_manager.h"
#include "chrome/browser/ash/policy/dlp/files_policy_notification_manager_factory.h"
#include "chrome/browser/enterprise/connectors/analysis/file_transfer_analysis_delegate.h"
#include "chrome/browser/enterprise/connectors/common.h"
#include "chrome/common/chrome_features.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/common/task_util.h"
#include "storage/browser/file_system/copy_or_move_hook_delegate_composite.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_operation.h"
#include "storage/browser/file_system/file_system_url.h"

namespace file_manager::io_task {

namespace {

// Scan the `idx`-th entry.
//
// Note: We include the previous_file_transfer_analysis_delegate here to manage
// its lifetime.
void DoReportOnlyScanning(
    std::unique_ptr<enterprise_connectors::FileTransferAnalysisDelegate>
        previous_file_transfer_analysis_delegate,
    size_t idx,
    std::vector<std::optional<enterprise_connectors::AnalysisSettings>>
        settings,
    std::vector<storage::FileSystemURL> sources,
    std::vector<storage::FileSystemURL> outputs,
    Profile* profile,
    scoped_refptr<storage::FileSystemContext> file_system_context) {
  previous_file_transfer_analysis_delegate.reset();

  if (idx >= settings.size()) {
    // Scanning is complete!
    return;
  }

  if (!settings[idx].has_value()) {
    // Don't scan this entry, but try the next.
    DoReportOnlyScanning(nullptr, idx + 1, std::move(settings),
                         std::move(sources), std::move(outputs), profile,
                         file_system_context);
    return;
  }

  std::unique_ptr<enterprise_connectors::FileTransferAnalysisDelegate>
      file_transfer_analysis_delegate =
          enterprise_connectors::FileTransferAnalysisDelegate::Create(
              safe_browsing::DeepScanAccessPoint::FILE_TRANSFER, sources[idx],
              outputs[idx], profile, file_system_context.get(),
              std::move(settings[idx].value()));

  // Manage lifetime of the file_transfer_analysis_delegate by binding it to the
  // completion callback.
  auto* file_transfer_analysis_delegate_ptr =
      file_transfer_analysis_delegate.get();
  file_transfer_analysis_delegate_ptr->UploadData(base::BindOnce(
      &DoReportOnlyScanning, std::move(file_transfer_analysis_delegate),
      idx + 1, std::move(settings), std::move(sources), std::move(outputs),
      profile, file_system_context));
}

// Start the asynchronous report-only scans.
//
// The `io_task_completion_callback` will be run before the scans are executed.
void StartReportOnlyScanning(
    IOTask::CompleteCallback io_task_completion_callback,
    std::vector<std::optional<enterprise_connectors::AnalysisSettings>>
        settings,
    Profile* profile,
    scoped_refptr<storage::FileSystemContext> file_system_context,
    ProgressStatus status) {
  // If there was an out-of-space error in the transfer, not all outputs might
  // be populated as the transfer is aborted on out-of-space errors.
  // So we truncate the settings, sources and outputs to only the first
  // `num_good_files` entries.
  const size_t num_good_files =
      std::min({settings.size(), status.sources.size(), status.outputs.size()});

  std::vector<storage::FileSystemURL> sources(num_good_files);
  std::vector<storage::FileSystemURL> outputs(num_good_files);
  for (size_t i = 0; i < num_good_files; ++i) {
    sources[i] = status.sources[i].url;
    outputs[i] = status.outputs[i].url;
  }
  settings.resize(num_good_files);

  // Notify the Files app of completion of the copy/move.
  std::move(io_task_completion_callback).Run(std::move(status));

  // Start the actual scanning.
  DoReportOnlyScanning(nullptr, 0, std::move(settings), std::move(sources),
                       std::move(outputs), profile, file_system_context);
}

// Notify FilesPolicyNotificationManager of files that were blocked by
// enterprise connectors to show proper blocked dialog.
// This is not done if the new UI for enterprise connectors is disabled.
void MaybeSendConnectorsBlockedFilesNotification(
    Profile* profile,
    std::map<policy::FilesPolicyDialog::BlockReason,
             policy::FilesPolicyDialog::Info> dialog_info_map,
    IOTaskId task_id,
    OperationType type) {
  if (dialog_info_map.empty()) {
    return;
  }

  // Blocked files are only added if kFileTransferEnterpriseConnectorUI is
  // enabled.
  CHECK(base::FeatureList::IsEnabled(
      features::kFileTransferEnterpriseConnectorUI));

  auto* files_policy_manager =
      policy::FilesPolicyNotificationManagerFactory::GetForBrowserContext(
          profile);
  if (!files_policy_manager) {
    LOG(ERROR) << "Couldn't find FilesPolicyNotificationManager";
    return;
  }

  for (const auto& [block_reason, dialog_info] : dialog_info_map) {
    files_policy_manager->SetConnectorsBlockedFiles(
        task_id,
        type == file_manager::io_task::OperationType::kMove
            ? policy::dlp::FileAction::kMove
            : policy::dlp::FileAction::kCopy,
        block_reason, std::move(dialog_info));
  }
}

}  // namespace

CopyOrMoveIOTaskPolicyImpl::CopyOrMoveIOTaskPolicyImpl(
    OperationType type,
    ProgressStatus& progress,
    std::vector<base::FilePath> destination_file_names,
    std::vector<std::optional<enterprise_connectors::AnalysisSettings>>
        settings,
    storage::FileSystemURL destination_folder,
    Profile* profile,
    scoped_refptr<storage::FileSystemContext> file_system_context,
    bool show_notification)
    : CopyOrMoveIOTaskImpl(type,
                           progress,
                           std::move(destination_file_names),
                           std::move(destination_folder),
                           profile,
                           file_system_context,
                           show_notification),
      profile_(profile),
      file_system_context_(file_system_context),
      settings_(std::move(settings)) {
  if (!settings_.empty()) {
    // The value of `block_until_verdict` is consistent for all settings, so we
    // just check the value for the first valid setting.
    auto valid_setting = base::ranges::find_if(
        settings_,
        [](const std::optional<enterprise_connectors::AnalysisSettings>&
               setting) { return setting.has_value(); });
    report_only_scans_ = valid_setting->value().block_until_verdict ==
                         enterprise_connectors::BlockUntilVerdict::kNoBlock;
  }
}

CopyOrMoveIOTaskPolicyImpl::~CopyOrMoveIOTaskPolicyImpl() = default;

void CopyOrMoveIOTaskPolicyImpl::Execute(
    IOTask::ProgressCallback progress_callback,
    IOTask::CompleteCallback complete_callback) {
  if (report_only_scans_) {
    // For report only scans, we perform the scans AFTER the transfer. So we
    // wrap the completion callback.
    CopyOrMoveIOTaskImpl::Execute(
        std::move(progress_callback),
        base::BindOnce(&StartReportOnlyScanning, std::move(complete_callback),
                       std::move(settings_), profile_, file_system_context_));
  } else {
    CopyOrMoveIOTaskImpl::Execute(std::move(progress_callback),
                                  std::move(complete_callback));
  }
}

void CopyOrMoveIOTaskPolicyImpl::Resume(ResumeParams params) {
  // In this class we only handle policy resumes, anything else defer to the
  // base class.
  if (!params.policy_params.has_value()) {
    CopyOrMoveIOTaskImpl::Resume(std::move(params));
    return;
  }

  auto* files_policy_manager =
      policy::FilesPolicyNotificationManagerFactory::GetForBrowserContext(
          profile_);
  if (!files_policy_manager) {
    LOG(ERROR) << "Couldn't find FilesPolicyNotificationManager";
    Complete(State::kError);
    return;
  }

  if (params.policy_params->type == policy::Policy::kDlp ||
      params.policy_params->type == policy::Policy::kEnterpriseConnectors) {
    files_policy_manager->OnIOTaskResumed(progress_->task_id);
  }
}

void CopyOrMoveIOTaskPolicyImpl::Complete(State state) {
  bool has_dlp_errors = !dlp_blocked_files_.empty();
  bool has_connector_errors = !connectors_blocked_files_.empty();
  if ((has_dlp_errors || has_connector_errors) &&
      base::FeatureList::IsEnabled(features::kNewFilesPolicyUX)) {
    // TODO(b/293425493): Support combined error type (if both dlp and connector
    // errors exist).
    PolicyErrorType error_type = has_dlp_errors
                                     ? PolicyErrorType::kDlp
                                     : PolicyErrorType::kEnterpriseConnectors;
    // Used for notifications.
    base::FilePath blocked_file_path =
        has_dlp_errors ? (*dlp_blocked_files_.begin())
                       : connectors_blocked_files_.begin()->second[0];
    std::string blocked_file_name =
        util::GetDisplayablePath(profile_, blocked_file_path)
            .value_or(base::FilePath())
            .BaseName()
            .value();
    bool always_show_review = false;

    std::map<policy::FilesPolicyDialog::BlockReason,
             policy::FilesPolicyDialog::Info>
        dialog_info_map;
    for (const auto& [reason, paths] : connectors_blocked_files_) {
      if (paths.empty()) {
        continue;
      }
      auto dialog_info = policy::files_dialog_utils::
          GetDialogInfoForEnterpriseConnectorsBlockReason(
              reason, paths, file_transfer_analysis_delegates_);
      always_show_review |= dialog_info.HasCustomDetails();
      dialog_info_map.insert({reason, std::move(dialog_info)});
    }

    progress_->policy_error.emplace(
        error_type,
        (dlp_blocked_files_.size() + GetConnectorsBlockedFilesNum()),
        blocked_file_name, always_show_review);
    state = State::kError;

    MaybeSendConnectorsBlockedFilesNotification(
        profile_, dialog_info_map, progress_->task_id, progress_->type);
  }

  CopyOrMoveIOTaskImpl::Complete(state);
}

void CopyOrMoveIOTaskPolicyImpl::VerifyTransfer() {
  auto on_check_transfer_cb =
      base::BindOnce(&CopyOrMoveIOTaskPolicyImpl::OnCheckIfTransferAllowed,
                     weak_ptr_factory_.GetWeakPtr());

  if (auto* files_controller =
          policy::DlpFilesControllerAsh::GetForPrimaryProfile();
      base::FeatureList::IsEnabled(features::kNewFilesPolicyUX) &&
      files_controller) {
    std::vector<storage::FileSystemURL> transferred_urls;
    for (const auto& entry : progress_->sources) {
      transferred_urls.push_back(entry.url);
    }
    bool is_move =
        progress_->type == file_manager::io_task::OperationType::kMove;
    files_controller->CheckIfTransferAllowed(
        progress_->task_id, std::move(transferred_urls),
        progress_->GetDestinationFolder(), is_move,
        std::move(on_check_transfer_cb));
    return;
  }

  std::move(on_check_transfer_cb).Run(/*blocked_entries=*/{});
}

storage::FileSystemOperation::ErrorBehavior
CopyOrMoveIOTaskPolicyImpl::GetErrorBehavior() {
  // This function is called when the transfer starts and DLP restrictions are
  // applied before the transfer. If there's any file blocked by DLP, the error
  // behavior should be skip instead of abort.
  if (report_only_scans_ && dlp_blocked_files_.empty()) {
    return storage::FileSystemOperation::ERROR_BEHAVIOR_ABORT;
  }
  // For the enterprise connectors, we want files to be copied/moved if they are
  // allowed and files to be prevented from copying/moving if they are blocked.
  // With `ERROR_BEHAVIOR_ABORT`, the first blocked file would result in the
  // copy/move operation to be aborted.
  // With `ERROR_BEHAVIOR_SKIP`, blocked files are ignored and all allowed files
  // will be copied.
  return storage::FileSystemOperation::ERROR_BEHAVIOR_SKIP;
}

std::unique_ptr<storage::CopyOrMoveHookDelegate>
CopyOrMoveIOTaskPolicyImpl::GetHookDelegate(size_t idx) {
  auto defaultHook = CopyOrMoveIOTaskImpl::GetHookDelegate(idx);

  if (settings_.empty() || report_only_scans_) {
    // For DLP only restrictions or report-only scans, no blocking should be
    // performed, so we use the normal delegate.
    return defaultHook;
  }

  DCHECK_LT(idx, file_transfer_analysis_delegates_.size());
  if (!file_transfer_analysis_delegates_[idx]) {
    // If scanning is disabled, use the normal delegate.
    // Scanning can be disabled if some source_urls lie on a file system for
    // which scanning is enabled, while other source_urls lie on a file system
    // for which scanning is disabled.
    return defaultHook;
  }

  // For all callbacks, we are using CreateRelayCallback to ensure that the
  // callbacks are executed on the current (i.e., UI) thread.
  auto file_check_callback = google_apis::CreateRelayCallback(
      base::BindRepeating(&CopyOrMoveIOTaskPolicyImpl::IsTransferAllowed,
                          weak_ptr_factory_.GetWeakPtr(), idx));
  auto checkHook = std::make_unique<FileManagerCopyOrMoveHookFileCheckDelegate>(
      file_system_context_, file_check_callback);
  return storage::CopyOrMoveHookDelegateComposite::CreateOrAdd(
      std::move(defaultHook), std::move(checkHook));
}

void CopyOrMoveIOTaskPolicyImpl::MaybeScanForDisallowedFiles(size_t idx) {
  DCHECK_LE(idx, progress_->sources.size());
  if (idx == progress_->sources.size()) {
    ScanningCompleted();
    return;
  }
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!settings_[idx].has_value()) {
    // Skip checking if connectors aren't enabled.
    MaybeScanForDisallowedFiles(idx + 1);
    return;
  }

  progress_->state = State::kScanning;
  progress_->sources_scanned = idx + 1;
  // TODO(b/315783871): recursively count the files in directories and pass this
  // value to the js side to show the proper singular/plural scanning label.
  progress_callback_.Run(*progress_);

  DCHECK_EQ(file_transfer_analysis_delegates_.size(),
            progress_->sources.size());

  file_transfer_analysis_delegates_[idx] =
      enterprise_connectors::FileTransferAnalysisDelegate::Create(
          safe_browsing::DeepScanAccessPoint::FILE_TRANSFER,
          progress_->sources[idx].url, progress_->GetDestinationFolder(),
          profile_, file_system_context_.get(),
          std::move(settings_[idx].value()));

  file_transfer_analysis_delegates_[idx]->UploadData(
      base::BindOnce(&CopyOrMoveIOTaskPolicyImpl::MaybeScanForDisallowedFiles,
                     weak_ptr_factory_.GetWeakPtr(), idx + 1));
}

void CopyOrMoveIOTaskPolicyImpl::ScanningCompleted() {
  if (!MaybeShowConnectorsWarning()) {
    // Only start the transfer if no warning was shown.
    // If a warning is shown, the transfer will be resumed or aborted through
    // the warning dialog/toasts/etc.
    StartTransfer();
  }
}

bool CopyOrMoveIOTaskPolicyImpl::MaybeShowConnectorsWarning() {
  bool connectors_new_ui_enabled = base::FeatureList::IsEnabled(
      features::kFileTransferEnterpriseConnectorUI);
  if (!connectors_new_ui_enabled) {
    return false;
  }

  std::vector<base::FilePath> warning_files_paths;
  for (const auto& delegate : file_transfer_analysis_delegates_) {
    if (!delegate) {
      continue;
    }
    for (const auto& warned_file : delegate->GetWarnedFiles()) {
      warning_files_paths.push_back(warned_file.path());
    }
  }

  if (warning_files_paths.empty()) {
    return false;
  }

  auto* fpnm =
      policy::FilesPolicyNotificationManagerFactory::GetForBrowserContext(
          profile_);
  if (!fpnm) {
    LOG(ERROR) << "No FilesPolicyNotificationManager instantiated,"
                  "can't show policy warning UI";
    return false;
  }

  // Currently, the custom warning message, the custom learn more URL, and the
  // bypass justification flag values are only relevant for warning scenarios.
  // Moreover, these values are consistent across all valid
  // `file_transfer_analysis_delegates_` having warned results, so we just
  // retrieve these values from the first of such delegates. There are as many
  // delegates as the number of sources. A delegate in
  // `file_transfer_analysis_delegates_` is valid if for the
  // source-destination-pair scanning is enabled, nullptr otherwise.
  auto delegate_it = base::ranges::find_if(
      file_transfer_analysis_delegates_,
      [](const std::unique_ptr<
          enterprise_connectors::FileTransferAnalysisDelegate>& delegate) {
        return delegate != nullptr;
      });

  // Warning mode is only available for the "dlp" tag (sensitive data), since
  // "malware" results are always blocked.
  auto dialog_info = policy::FilesPolicyDialog::Info::Warn(
      policy::FilesPolicyDialog::BlockReason::
          kEnterpriseConnectorsSensitiveData,
      warning_files_paths);
  if (delegate_it != file_transfer_analysis_delegates_.end()) {
    auto* valid_delegate = delegate_it->get();
    dialog_info.SetBypassRequiresJustification(
        valid_delegate->BypassRequiresJustification(
            enterprise_connectors::kDlpTag));
    dialog_info.SetMessage(
        valid_delegate->GetCustomMessage(enterprise_connectors::kDlpTag));
    dialog_info.SetLearnMoreURL(
        valid_delegate->GetCustomLearnMoreUrl(enterprise_connectors::kDlpTag));
  }

  fpnm->ShowConnectorsWarning(
      base::BindOnce(&CopyOrMoveIOTaskPolicyImpl::OnConnectorsWarnDialogResult,
                     weak_ptr_factory_.GetWeakPtr()),
      std::move(progress_->task_id),
      progress_->type == file_manager::io_task::OperationType::kMove
          ? policy::dlp::FileAction::kMove
          : policy::dlp::FileAction::kCopy,
      std::move(dialog_info));
  return true;
}

void CopyOrMoveIOTaskPolicyImpl::OnConnectorsWarnDialogResult(
    std::optional<std::u16string> user_justification,
    bool should_proceed) {
  if (!should_proceed) {
    // No need to cancel. Cancel will be called from
    // FilesPolicyNotificationManager.
    return;
  }
  // If the user has proceeded the warning, then we need to notify the
  // `FileTransferAnalysisDelegate`s to report the bypass of the warning and to
  // mark warned files as allowed for a transfer.
  base::ranges::for_each(file_transfer_analysis_delegates_,
                         [&user_justification](const auto& delegate) {
                           if (delegate) {
                             delegate->BypassWarnings(user_justification);
                           }
                         });
  StartTransfer();
}

void CopyOrMoveIOTaskPolicyImpl::IsTransferAllowed(
    size_t idx,
    const storage::FileSystemURL& source_url,
    const storage::FileSystemURL& destination_url,
    IsTransferAllowedCallback callback) {
  DCHECK(!report_only_scans_);
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(file_transfer_analysis_delegates_[idx]);
  auto result =
      file_transfer_analysis_delegates_[idx]->GetAnalysisResultAfterScan(
          source_url);
  // If a file is blocked by DLP, skip Enterprise Connectors scanning for it
  // since Enterprise Connectors won't be able to scan it anyway. The file be
  // blocked by the DLP daemon later.
  if (result.IsAllowed() ||
      base::Contains(dlp_blocked_files_, source_url.path())) {
    std::move(callback).Run(base::File::FILE_OK);
    return;
  }
  DCHECK(result.IsUnknown() || result.IsBlocked());

  if (base::FeatureList::IsEnabled(
          features::kFileTransferEnterpriseConnectorUI)) {
    auto& paths = connectors_blocked_files_
        [policy::files_dialog_utils::GetEnterpriseConnectorsBlockReason(
            result)];
    paths.push_back(source_url.path());
  }

  std::move(callback).Run(base::File::FILE_ERROR_SECURITY);
}

void CopyOrMoveIOTaskPolicyImpl::OnCheckIfTransferAllowed(
    std::vector<storage::FileSystemURL> blocked_entries) {
  // Check if the task was cancelled by the user.
  if (progress_->state == State::kCancelled) {
    return;
  }

  for (const auto& entry : blocked_entries) {
    dlp_blocked_files_.insert(entry.path());
  }

  if (settings_.empty() || report_only_scans_) {
    // Re-enter state progress if needed.
    if (progress_->state != State::kInProgress) {
      progress_->state = State::kInProgress;
      progress_callback_.Run(*progress_);
    }
    // Don't do any scans. It's either dlp-only restrictions (if `settings_` is
    // empty), or the scans will performed after the copy/move is completed
    // (report_only_scans_ is true).
    StartTransfer();
    return;
  }

  // Allocate one unique_ptr for each source. If it is not set, scanning is not
  // enabled for this source.
  file_transfer_analysis_delegates_.resize(progress_->sources.size());
  MaybeScanForDisallowedFiles(0);
}

size_t CopyOrMoveIOTaskPolicyImpl::GetConnectorsBlockedFilesNum() const {
  size_t num = 0;
  for (const auto& [_, paths] : connectors_blocked_files_) {
    num += paths.size();
  }
  return num;
}

}  // namespace file_manager::io_task