chromium/chrome/browser/ash/policy/dlp/dlp_files_controller_ash.cc

// Copyright 2021 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/policy/dlp/dlp_files_controller_ash.h"

#include <sys/types.h>

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/file_utils.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/file_system_provider/service.h"
#include "chrome/browser/ash/policy/dlp/dlp_extract_io_task_observer.h"
#include "chrome/browser/ash/policy/dlp/dlp_files_controller_ash_utils.h"
#include "chrome/browser/ash/policy/dlp/dlp_files_event_storage.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/chromeos/policy/dlp/dlp_file_destination.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_files_utils.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_policy_constants.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager_factory.h"
#include "chrome/browser/enterprise/data_controls/dlp_reporting_manager.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chromeos/dbus/dlp/dlp_client.h"
#include "chromeos/dbus/dlp/dlp_service.pb.h"
#include "chromeos/ui/base/file_icon_util.h"
#include "components/enterprise/data_controls/core/browser/component.h"
#include "components/enterprise/data_controls/core/browser/dlp_histogram_helper.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/common/constants.h"
#include "url/gurl.h"

namespace policy {
namespace {

// System application URLs.
// Please keep them updated with dlp_files_controller_ash_unittest.cc.
constexpr char kFileManagerUrl[] = "chrome://file-manager/";
constexpr char kImageLoaderUrl[] =
    "chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp/";

// Timeout defining when two events having the same properties are considered
// duplicates.
// TODO(crbug.com/1368982): determine the value to use.
constexpr base::TimeDelta kCooldownTimeout = base::Seconds(5);

// The maximum number of entries that can be kept in the
// DlpFilesEventStorage.
// TODO(crbug.com/1366299): determine the value to use.
constexpr size_t kEntriesLimit = 100;

// Returns a `DlpFileDestination` with a source URL or component, based on
// |app_update|. If neither URL nor component can be found, returns nullopt.
std::optional<DlpFileDestination> GetFileDestinationForApp(
    const apps::AppUpdate& app_update) {
  switch (app_update.AppType()) {
    case apps::AppType::kStandaloneBrowserChromeApp:
    case apps::AppType::kExtension:
    case apps::AppType::kStandaloneBrowserExtension:
    case apps::AppType::kChromeApp:
      return DlpFileDestination(GURL(base::StrCat(
          {extensions::kExtensionScheme, "://", app_update.AppId()})));
    case apps::AppType::kArc:
      return DlpFileDestination(data_controls::Component::kArc);
    case apps::AppType::kCrostini:
      return DlpFileDestination(data_controls::Component::kCrostini);
    case apps::AppType::kPluginVm:
      return DlpFileDestination(data_controls::Component::kPluginVm);
    case apps::AppType::kWeb:
    case apps::AppType::kSystemWeb:
      // Expecting `PublisherId()` to return an URL. For web apps this should be
      // the start URL.
      return DlpFileDestination(GURL(app_update.PublisherId()));
    case apps::AppType::kUnknown:
    case apps::AppType::kBuiltIn:
    case apps::AppType::kStandaloneBrowser:
    case apps::AppType::kRemote:
    case apps::AppType::kBorealis:
    case apps::AppType::kBruschetta:
      return std::nullopt;
  }
  return std::nullopt;
}

// Converts files paths to file system URLs.
std::vector<storage::FileSystemURL> ConvertFilePathsToFileSystemUrls(
    Profile* profile,
    const storage::FileSystemContext& file_system_context,
    const std::vector<base::FilePath>& files_paths) {
  std::vector<storage::FileSystemURL> file_system_urls;

  for (const auto& file_path : files_paths) {
    GURL gurl;
    if (file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
            profile, file_path, file_manager::util::GetFileManagerURL(),
            &gurl)) {
      file_system_urls.push_back(
          file_system_context.CrackURLInFirstPartyContext(gurl));
    }
  }

  return file_system_urls;
}

file_manager::VolumeManager* GetVolumeManager(
    content::BrowserContext* context) {
  CHECK(context);

  file_manager::VolumeManager* const volume_manager =
      file_manager::VolumeManager::Get(context);
  if (!volume_manager) {
    return nullptr;
  }
  return volume_manager;
}

// Returns whether `url` represents the URL of a system application.
bool IsSystemAppURL(const GURL& url) {
  static constexpr auto kSystemURLsMap =
      base::MakeFixedFlatSet<std::string_view>(
          {kFileManagerUrl, kImageLoaderUrl});
  return kSystemURLsMap.contains(url.spec());
}

// Return converted `level`. It is converted to kBlock if it is `kWarn` and the
// destination is a system app to avoid spamming the user with warning requests
// for browsing a folder with warned (image) files.
DlpRulesManager::Level ConvertSystemAppWarning(
    DlpRulesManager::Level level,
    const DlpFileDestination& destination) {
  if (level == DlpRulesManager::Level::kWarn && destination.url() &&
      IsSystemAppURL(*destination.url())) {
    return DlpRulesManager::Level::kBlock;
  }
  return level;
}

}  // namespace

// static
DlpFilesControllerAsh* DlpFilesControllerAsh::GetForPrimaryProfile() {
  DlpRulesManager* rules_manager =
      DlpRulesManagerFactory::GetForPrimaryProfile();
  return static_cast<DlpFilesControllerAsh*>(
      rules_manager ? rules_manager->GetDlpFilesController() : nullptr);
}

DlpFilesControllerAsh::DlpFileMetadata::DlpFileMetadata(
    const std::string& source_url,
    const std::string& referrer_url,
    bool is_dlp_restricted,
    bool is_restricted_for_destination)
    : source_url(source_url),
      referrer_url(referrer_url),
      is_dlp_restricted(is_dlp_restricted),
      is_restricted_for_destination(is_restricted_for_destination) {}

DlpFilesControllerAsh::DlpFileRestrictionDetails::DlpFileRestrictionDetails() =
    default;

DlpFilesControllerAsh::DlpFileRestrictionDetails::DlpFileRestrictionDetails(
    DlpFileRestrictionDetails&&) = default;
DlpFilesControllerAsh::DlpFileRestrictionDetails&
DlpFilesControllerAsh::DlpFileRestrictionDetails::operator=(
    DlpFilesControllerAsh::DlpFileRestrictionDetails&&) = default;

DlpFilesControllerAsh::DlpFileRestrictionDetails::~DlpFileRestrictionDetails() =
    default;

DlpFilesControllerAsh::DlpFilesControllerAsh(
    const DlpRulesManager& rules_manager,
    Profile* profile)
    : DlpFilesController(rules_manager),
      profile_(profile),
      event_storage_(std::make_unique<DlpFilesEventStorage>(kCooldownTimeout,
                                                            kEntriesLimit)) {
  CHECK(profile_);

  auto* volume_manager = GetVolumeManager(profile_);
  if (!volume_manager) {
    LOG(ERROR)
        << "DlpFilesControllerAsh failed to find file_manager::VolumeManager";
    return;
  }

  volume_manager->AddObserver(this);

  auto* io_task_controller = volume_manager->io_task_controller();
  if (!io_task_controller) {
    LOG(ERROR) << "DlpFilesControllerAsh failed to find "
                  "file_manager::io_task::IOTaskController";
    return;
  }
  extract_io_task_observer_ =
      std::make_unique<DlpExtractIOTaskObserver>(*io_task_controller);
}

DlpFilesControllerAsh::~DlpFilesControllerAsh() {
  if (extract_io_task_observer_) {
    // If `extract_io_task_observer_` is still alive, it means we are deleting
    // FilesController before VolumeManager, otherwise we would have been
    // notified in `OnShutdownStart`.
    auto* volume_manager = GetVolumeManager(profile_);
    if (volume_manager) {
      volume_manager->RemoveObserver(this);
    }
  }
}

void DlpFilesControllerAsh::CheckIfTransferAllowed(
    std::optional<file_manager::io_task::IOTaskId> task_id,
    const std::vector<storage::FileSystemURL>& transferred_files,
    storage::FileSystemURL destination,
    bool is_move,
    CheckIfTransferAllowedCallback result_callback) {
  auto* file_system_context = GetFileSystemContextForPrimaryProfile();
  if (!file_system_context) {
    std::move(result_callback).Run(std::vector<storage::FileSystemURL>());
    return;
  }

  // If the destination file path is in MyFiles, all files transfers should be
  // allowed.
  if (IsInLocalFileSystem(destination.path())) {
    std::move(result_callback).Run(std::vector<storage::FileSystemURL>());
    return;
  }

  std::vector<storage::FileSystemURL> filtered_files;
  // If the copied file isn't in the local file system, or the file is in the
  // same file system as the destination, no restrictions should be applied.
  for (const auto& file : transferred_files) {
    if (!IsInLocalFileSystem(file.path()) ||
        file.IsInSameFileSystem(destination)) {
      continue;
    }
    filtered_files.push_back(file);
  }
  if (filtered_files.empty()) {
    std::move(result_callback).Run(std::vector<storage::FileSystemURL>());
    return;
  }

  auto* roots_recursion_delegate = new RootsRecursionDelegate(
      file_system_context, std::move(filtered_files),
      base::BindOnce(&DlpFilesControllerAsh::ContinueCheckIfTransferAllowed,
                     weak_ptr_factory_.GetWeakPtr(), std::move(task_id),
                     std::move(destination), is_move,
                     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)));
}

void DlpFilesControllerAsh::GetDlpMetadata(
    const std::vector<storage::FileSystemURL>& files,
    std::optional<DlpFileDestination> destination,
    GetDlpMetadataCallback result_callback) {
  if (!chromeos::DlpClient::Get() || !chromeos::DlpClient::Get()->IsAlive()) {
    std::move(result_callback).Run(std::vector<DlpFileMetadata>());
    return;
  }

  ::dlp::GetFilesSourcesRequest request;
  for (const auto& file : files) {
    if (IsInLocalFileSystem(file.path())) {
      request.add_files_paths(file.path().value());
    }
  }
  chromeos::DlpClient::Get()->GetFilesSources(
      request, base::BindOnce(&DlpFilesControllerAsh::ReturnDlpMetadata,
                              weak_ptr_factory_.GetWeakPtr(), std::move(files),
                              destination, std::move(result_callback)));
}

void DlpFilesControllerAsh::FilterDisallowedUploads(
    std::vector<ui::SelectedFileInfo> selected_files,
    const DlpFileDestination& destination,
    FilterDisallowedUploadsCallback result_callback) {
  if (selected_files.empty()) {
    std::move(result_callback).Run(std::move(selected_files));
    return;
  }

  auto* file_system_context = GetFileSystemContextForPrimaryProfile();
  if (!file_system_context) {
    std::move(result_callback).Run(std::move(selected_files));
    return;
  }

  std::vector<base::FilePath> files_paths;
  for (const auto& file : selected_files) {
    files_paths.push_back(file.local_path.empty() ? file.file_path
                                                  : file.local_path);
  }
  std::vector<storage::FileSystemURL> file_system_urls =
      ConvertFilePathsToFileSystemUrls(profile_, *file_system_context,
                                       files_paths);

  if (file_system_urls.empty()) {
    std::move(result_callback).Run(std::move(selected_files));
    return;
  }

  auto* roots_recursion_delegate = new RootsRecursionDelegate(
      file_system_context, std::move(file_system_urls),
      base::BindOnce(&DlpFilesControllerAsh::ContinueFilterDisallowedUploads,
                     weak_ptr_factory_.GetWeakPtr(), std::move(selected_files),
                     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)));
}

void DlpFilesControllerAsh::CheckIfDownloadAllowed(
    const DlpFileDestination& download_src,
    const base::FilePath& file_path,
    CheckIfDlpAllowedCallback result_callback) {
  if (!download_src.url().has_value()) {
    // Currently we only support urls as sources.
    std::move(result_callback).Run(true);
    return;
  }

  auto dst_component =
      MapFilePathToPolicyComponent(profile_, base::FilePath(file_path));
  if (!dst_component.has_value()) {
    // We may block downloads only if saved to external component, otherwise
    // downloads should be allowed.
    std::move(result_callback).Run(true);
    return;
  }

  DlpFileDestination dlp_destination = DlpFileDestination(*dst_component);
  // TODO(b/290200170): Check whether referrer_url could be set too.
  FileDaemonInfo file_info(/*inode=*/{}, /*crtime=*/{}, file_path,
                           download_src.url()->spec(),
                           /*referrer_url=*/"");

  IsFilesTransferRestricted(
      std::nullopt, {std::move(file_info)}, dlp_destination,
      dlp::FileAction::kDownload,
      base::BindOnce(
          [](CheckIfDlpAllowedCallback result_callback, Profile* profile,
             const std::vector<std::pair<
                 FileDaemonInfo, ::dlp::RestrictionLevel>>& files_levels) {
            bool is_allowed = true;
            base::FilePath file_path;
            for (const auto& [file, level] : files_levels) {
              if (level == ::dlp::RestrictionLevel::LEVEL_BLOCK ||
                  level == ::dlp::RestrictionLevel::LEVEL_WARN_CANCEL) {
                is_allowed = false;
                file_path = file.path;
                break;
              }
            }
            if (!is_allowed) {
              ::policy::files_controller_ash_utils::ShowDlpBlockedFiles(
                  profile, /*task_id=*/std::nullopt, {file_path},
                  dlp::FileAction::kDownload);
            }
            std::move(result_callback).Run(is_allowed);
          },
          std::move(result_callback), profile_));
}

bool DlpFilesControllerAsh::ShouldPromptBeforeDownload(
    const DlpFileDestination& download_src,
    const base::FilePath& file_path) {
  if (download_src.IsFileSystem()) {
    return false;
  }
  auto dst_component =
      MapFilePathToPolicyComponent(profile_, base::FilePath(file_path));
  if (!dst_component.has_value()) {
    // We may block downloads only if saved to external component, otherwise
    // downloads should be allowed.
    return false;
  }

  DlpRulesManager::Level level = rules_manager_->IsRestrictedComponent(
      download_src.url().value(), dst_component.value(),
      DlpRulesManager::Restriction::kFiles, /*out_source_pattern=*/nullptr,
      /*out_rule_metadata=*/nullptr);
  return level == DlpRulesManager::Level::kBlock ||
         level == DlpRulesManager::Level::kWarn;
}

void DlpFilesControllerAsh::CheckIfLaunchAllowed(
    const apps::AppUpdate& app_update,
    apps::IntentPtr intent,
    CheckIfDlpAllowedCallback result_callback) {
  if (intent->files.empty()) {
    std::move(result_callback).Run(/*is_allowed=*/true);
    return;
  }
  std::optional<DlpFileDestination> destination =
      GetFileDestinationForApp(app_update);
  if (!destination.has_value()) {
    std::move(result_callback).Run(/*is_allowed=*/true);
    return;
  }
  CHECK(!destination->IsMyFiles());

  ::dlp::CheckFilesTransferRequest request;
  for (const auto& file : intent->files) {
    auto file_url = apps::GetFileSystemURL(profile_, file->url);
    request.add_files_paths(file_url.path().value());
  }

  request.set_file_action(intent->IsShareIntent() ? ::dlp::FileAction::SHARE
                                                  : ::dlp::FileAction::OPEN);

  if (destination->url().has_value()) {
    request.set_destination_url(destination->url()->spec());
  } else {  // component
    request.set_destination_component(
        dlp::MapPolicyComponentToProto(destination->component().value()));
  }

  chromeos::DlpClient::Get()->CheckFilesTransfer(
      request,
      base::BindOnce(&DlpFilesControllerAsh::ReturnIfActionAllowed,
                     weak_ptr_factory_.GetWeakPtr(), dlp::FileAction::kOpen,
                     std::move(result_callback)));
}

bool DlpFilesControllerAsh::IsLaunchBlocked(const apps::AppUpdate& app_update,
                                            const apps::IntentPtr& intent) {
  if (intent->files.empty()) {
    return false;
  }

  std::optional<DlpFileDestination> destination =
      GetFileDestinationForApp(app_update);

  if (!destination.has_value()) {
    return false;
  }

  for (const auto& file : intent->files) {
    if (!file->dlp_source_url.has_value()) {
      continue;
    }
    if (destination->url().has_value()) {
      DlpRulesManager::Level level = rules_manager_->IsRestrictedDestination(
          GURL(file->dlp_source_url.value()), *destination->url(),
          DlpRulesManager::Restriction::kFiles, /*out_source_pattern=*/nullptr,
          /*out_destination_pattern=*/nullptr, /*out_rule_metadata=*/nullptr);
      if (level == DlpRulesManager::Level::kBlock) {
        return true;
      }
    } else if (destination->component().has_value()) {
      DlpRulesManager::Level level = rules_manager_->IsRestrictedComponent(
          GURL(file->dlp_source_url.value()), destination->component().value(),
          DlpRulesManager::Restriction::kFiles, /*out_source_pattern=*/nullptr,
          /*out_rule_metadata=*/nullptr);
      if (level == DlpRulesManager::Level::kBlock) {
        return true;
      }
    }
  }

  return false;
}

void DlpFilesControllerAsh::IsFilesTransferRestricted(
    std::optional<file_manager::io_task::IOTaskId> task_id,
    const std::vector<FileDaemonInfo>& transferred_files,
    const DlpFileDestination& destination,
    dlp::FileAction files_action,
    IsFilesTransferRestrictedCallback result_callback) {
  DlpFileDestination actual_dst = destination;

  std::vector<std::pair<FileDaemonInfo, ::dlp::RestrictionLevel>> files_levels;
  std::vector<FileDaemonInfo> warned_files;
  std::optional<std::string> destination_pattern;
  std::vector<std::string> warned_source_patterns;
  std::vector<DlpRulesManager::RuleMetadata> warned_rules_metadata;
  for (const auto& file : transferred_files) {
    DlpRulesManager::Level level;
    std::string source_pattern;
    DlpRulesManager::RuleMetadata rule_metadata;
    if (destination.component()) {
      data_controls::Component dst_component = *destination.component();
      level = rules_manager_->IsRestrictedComponent(
          file.source_url, dst_component, DlpRulesManager::Restriction::kFiles,
          &source_pattern, &rule_metadata);
      actual_dst = DlpFileDestination(dst_component);
      MaybeReportEvent(file.inode, file.crtime, file.path,
                       file.source_url.spec(), actual_dst, std::nullopt,
                       rule_metadata, level);
    } else if (destination.IsFileSystem()) {
      level = DlpRulesManager::Level::kAllow;
    } else {
      DCHECK(destination.url().has_value());
      destination_pattern = std::string();
      level = rules_manager_->IsRestrictedDestination(
          GURL(file.source_url), GURL(*destination.url()),
          DlpRulesManager::Restriction::kFiles, &source_pattern,
          &destination_pattern.value(), &rule_metadata);
      if (!IsSystemAppURL(destination.url().value())) {
        MaybeReportEvent(file.inode, file.crtime, file.path,
                         file.source_url.spec(), actual_dst,
                         destination_pattern, rule_metadata, level);
      }
    }

    level = ConvertSystemAppWarning(level, destination);

    switch (level) {
      case DlpRulesManager::Level::kBlock: {
        files_levels.emplace_back(file, ::dlp::RestrictionLevel::LEVEL_BLOCK);
        break;
      }
      case DlpRulesManager::Level::kNotSet:
      case DlpRulesManager::Level::kAllow: {
        files_levels.emplace_back(file, ::dlp::RestrictionLevel::LEVEL_ALLOW);
        break;
      }
      case DlpRulesManager::Level::kReport: {
        files_levels.emplace_back(file, ::dlp::RestrictionLevel::LEVEL_REPORT);
        break;
      }
      case DlpRulesManager::Level::kWarn: {
        warned_files.push_back(file);
        warned_source_patterns.emplace_back(source_pattern);
        warned_rules_metadata.emplace_back(rule_metadata);
        break;
      }
    }
  }

  if (warned_files.empty()) {
    std::move(result_callback).Run(std::move(files_levels));
    return;
  }

  auto* fpnm =
      FilesPolicyNotificationManagerFactory::GetForBrowserContext(profile_);
  if (!fpnm) {
    LOG(ERROR) << "No FilesPolicyNotificationManager instantiated,"
                  "can't show policy warning UI";
    return;
  }
  std::vector<base::FilePath> warning_files_paths;
  base::ranges::for_each(warned_files, [&](auto& warned_file) {
    warning_files_paths.push_back(warned_file.path);
  });
  fpnm->ShowDlpWarning(
      base::BindOnce(&DlpFilesControllerAsh::OnDlpWarnDialogReply,
                     weak_ptr_factory_.GetWeakPtr(), std::move(files_levels),
                     std::move(warned_files), std::move(warned_source_patterns),
                     std::move(warned_rules_metadata), actual_dst,
                     destination_pattern, files_action,
                     std::move(result_callback)),
      std::move(task_id), std::move(warning_files_paths), actual_dst,
      files_action);
}

std::vector<DlpFilesControllerAsh::DlpFileRestrictionDetails>
DlpFilesControllerAsh::GetDlpRestrictionDetails(const std::string& source_url) {
  const GURL source(source_url);
  const DlpRulesManager::AggregatedDestinations aggregated_destinations =
      rules_manager_->GetAggregatedDestinations(
          source, DlpRulesManager::Restriction::kFiles);
  const DlpRulesManager::AggregatedComponents aggregated_components =
      rules_manager_->GetAggregatedComponents(
          source, DlpRulesManager::Restriction::kFiles);

  std::vector<DlpFilesControllerAsh::DlpFileRestrictionDetails> result;
  // Add levels for which urls are set.
  for (const auto& [level, urls] : aggregated_destinations) {
    DlpFileRestrictionDetails details;
    details.level = level;
    base::ranges::move(urls.begin(), urls.end(),
                       std::back_inserter(details.urls));
    // Add the components for this level, if any.
    const auto it = aggregated_components.find(level);
    if (it != aggregated_components.end()) {
      base::ranges::move(it->second.begin(), it->second.end(),
                         std::back_inserter(details.components));
    }
    result.emplace_back(std::move(details));
  }

  // There might be levels for which only components are set, so we need to add
  // those separately.
  for (const auto& [level, components] : aggregated_components) {
    if (aggregated_destinations.find(level) != aggregated_destinations.end()) {
      // Already added in the previous loop.
      continue;
    }
    DlpFileRestrictionDetails details;
    details.level = level;
    base::ranges::move(components.begin(), components.end(),
                       std::back_inserter(details.components));
    result.emplace_back(std::move(details));
  }

  return result;
}

std::vector<data_controls::Component>
DlpFilesControllerAsh::GetBlockedComponents(const std::string& source_url) {
  const GURL source(source_url);
  const DlpRulesManager::AggregatedComponents aggregated_components =
      rules_manager_->GetAggregatedComponents(
          source, DlpRulesManager::Restriction::kFiles);

  std::vector<data_controls::Component> result;
  const auto it = aggregated_components.find(DlpRulesManager::Level::kBlock);
  if (it != aggregated_components.end()) {
    base::ranges::move(it->second.begin(), it->second.end(),
                       std::back_inserter(result));
  }
  return result;
}

bool DlpFilesControllerAsh::IsDlpPolicyMatched(const FileDaemonInfo& file) {
  bool restricted = false;

  std::string src_pattern;
  DlpRulesManager::RuleMetadata rule_metadata;
  policy::DlpRulesManager::Level level = rules_manager_->IsRestrictedByAnyRule(
      GURL(file.source_url.spec()),
      policy::DlpRulesManager::Restriction::kFiles, &src_pattern,
      &rule_metadata);

  switch (level) {
    case policy::DlpRulesManager::Level::kBlock:
      restricted = true;
      break;
    case policy::DlpRulesManager::Level::kWarn:
      // Normally this case should not be hit as it means that a restricted file
      // was accessed by a flow without requesting access before. We protect
      // warned files the same way as blocked to not allow unauthorized access.
      restricted = true;
      break;
    default:
      break;
  }

  data_controls::DlpHistogramEnumeration(
      data_controls::dlp::kFilesUnknownAccessLevel, level);

  return restricted;
}

void DlpFilesControllerAsh::OnShutdownStart(
    file_manager::VolumeManager* volume_manager) {
  volume_manager->RemoveObserver(this);
  // IOTaskController is destroyed at VolumeManager deletion. Delete the
  // observer since it depends on IOTaskController.
  extract_io_task_observer_.reset();
}

DlpFilesEventStorage* DlpFilesControllerAsh::GetEventStorageForTesting() {
  return event_storage_.get();
}

std::optional<data_controls::Component>
DlpFilesControllerAsh::MapFilePathToPolicyComponent(
    Profile* profile,
    const base::FilePath& file_path) {
  if (base::FilePath(file_manager::util::GetAndroidFilesPath())
          .IsParent(file_path)) {
    return data_controls::Component::kArc;
  }

  if (base::FilePath(file_manager::util::kRemovableMediaPath)
          .IsParent(file_path)) {
    return data_controls::Component::kUsb;
  }

  drive::DriveIntegrationService* integration_service =
      drive::DriveIntegrationServiceFactory::FindForProfile(profile);
  if (integration_service && integration_service->is_enabled() &&
      integration_service->GetMountPointPath().IsParent(file_path)) {
    return data_controls::Component::kDrive;
  }

  if (ash::cloud_upload::IsODFSMounted(profile)) {
    auto* service = ash::file_system_provider::Service::Get(profile);
    auto provider_id =
        ash::file_system_provider::ProviderId::CreateFromExtensionId(
            extension_misc::kODFSExtensionId);
    auto one_drive_file_systems =
        service->GetProvidedFileSystemInfoList(provider_id);
    CHECK_EQ(one_drive_file_systems.size(), 1u);

    if (one_drive_file_systems[0].mount_path().IsParent(file_path)) {
      return data_controls::Component::kOneDrive;
    }
  }

  base::FilePath linux_files =
      file_manager::util::GetCrostiniMountDirectory(profile);
  if (linux_files == file_path || linux_files.IsParent(file_path)) {
    return data_controls::Component::kCrostini;
  }

  return {};
}

bool DlpFilesControllerAsh::IsInLocalFileSystem(
    const base::FilePath& file_path) {
  auto my_files_folder =
      file_manager::util::GetMyFilesFolderForProfile(profile_);
  if (my_files_folder == file_path || my_files_folder.IsParent(file_path)) {
    return true;
  }
  return false;
}

void DlpFilesControllerAsh::ShowDlpBlockedFiles(
    std::optional<file_manager::io_task::IOTaskId> task_id,
    std::vector<base::FilePath> blocked_files,
    dlp::FileAction action) {
  ::policy::files_controller_ash_utils::ShowDlpBlockedFiles(
      profile_, std::move(task_id), std::move(blocked_files), action);
}

void DlpFilesControllerAsh::OnDlpWarnDialogReply(
    std::vector<std::pair<FileDaemonInfo, ::dlp::RestrictionLevel>>
        files_levels,
    std::vector<FileDaemonInfo> warned_files,
    std::vector<std::string> warned_src_patterns,
    std::vector<DlpRulesManager::RuleMetadata> warned_rules_metadata,
    const DlpFileDestination& dst,
    const std::optional<std::string>& dst_pattern,
    dlp::FileAction files_action,
    IsFilesTransferRestrictedCallback callback,
    std::optional<std::u16string> user_justification,
    bool should_proceed) {
  DCHECK(warned_files.size() == warned_src_patterns.size());
  DCHECK(warned_files.size() == warned_rules_metadata.size());
  for (size_t i = 0; i < warned_files.size(); ++i) {
    if (should_proceed) {
      data_controls::DlpHistogramEnumeration(
          data_controls::dlp::kFileActionWarnProceededUMA, files_action);
      MaybeReportEvent(warned_files[i].inode, warned_files[i].crtime,
                       warned_files[i].path, warned_files[i].source_url.spec(),
                       dst, dst_pattern, warned_rules_metadata[i],
                       std::nullopt);
    }
    files_levels.emplace_back(warned_files[i],
                              should_proceed
                                  ? ::dlp::RestrictionLevel::LEVEL_WARN_PROCEED
                                  : ::dlp::RestrictionLevel::LEVEL_WARN_CANCEL);
  }
  std::move(callback).Run(std::move(files_levels));
}

void DlpFilesControllerAsh::ReturnDisallowedFiles(
    std::optional<file_manager::io_task::IOTaskId> task_id,
    base::flat_map<std::string, storage::FileSystemURL> files_map,
    dlp::FileAction file_action,
    CheckIfTransferAllowedCallback result_callback,
    ::dlp::CheckFilesTransferResponse response) {
  std::vector<storage::FileSystemURL> restricted_files_urls;
  std::vector<base::FilePath> restricted_files_paths;
  if (response.has_error_message()) {
    LOG(ERROR) << "Failed to get check files transfer, error: "
               << response.error_message();
    for (const auto& [file_path, file_system_url] : files_map) {
      restricted_files_urls.push_back(file_system_url);
    }
    std::move(result_callback).Run(std::move(restricted_files_urls));
    return;
  }

  for (const auto& file : response.files_paths()) {
    DCHECK(files_map.find(file) != files_map.end());
    restricted_files_urls.push_back(files_map.at(file));
    restricted_files_paths.emplace_back(file);
  }
  if (!restricted_files_paths.empty() &&
      base::FeatureList::IsEnabled(features::kNewFilesPolicyUX) &&
      task_id.has_value()) {
    ::policy::files_controller_ash_utils::ShowDlpBlockedFiles(
        profile_, std::move(task_id), std::move(restricted_files_paths),
        file_action);
  }
  std::move(result_callback).Run(std::move(restricted_files_urls));
}

void DlpFilesControllerAsh::ReturnAllowedUploads(
    std::vector<ui::SelectedFileInfo> selected_files,
    FilterDisallowedUploadsCallback 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(std::move(selected_files));
    return;
  }

  if (!response.files_paths().empty()) {
    std::vector<base::FilePath> restricted_files(response.files_paths().begin(),
                                                 response.files_paths().end());
    // If any of the selected files/folders is restricted or contains a
    // restricted file, it'll be removed.
    std::erase_if(
        selected_files,
        [&restricted_files](const ui::SelectedFileInfo& selected_file) -> bool {
          return base::ranges::any_of(
              restricted_files, [&](const base::FilePath& restricted_file) {
                return selected_file.file_path == restricted_file ||
                       selected_file.file_path.IsParent(restricted_file);
              });
        });

    ::policy::files_controller_ash_utils::ShowDlpBlockedFiles(
        profile_, /*task_id=*/std::nullopt, std::move(restricted_files),
        dlp::FileAction::kUpload);
  }

  std::move(result_callback).Run(std::move(selected_files));
}

void DlpFilesControllerAsh::ReturnDlpMetadata(
    const std::vector<storage::FileSystemURL>& files,
    std::optional<DlpFileDestination> destination,
    GetDlpMetadataCallback result_callback,
    const ::dlp::GetFilesSourcesResponse response) {
  if (response.has_error_message()) {
    LOG(ERROR) << "Failed to get files sources, error: "
               << response.error_message();
  }

  base::flat_map<std::string, DlpFileMetadata> metadata_map;
  for (const auto& metadata : response.files_metadata()) {
    DlpRulesManager::Level level = rules_manager_->IsRestrictedByAnyRule(
        GURL(metadata.source_url()), DlpRulesManager::Restriction::kFiles,
        nullptr, nullptr);
    bool is_dlp_restricted = level != DlpRulesManager::Level::kNotSet &&
                             level != DlpRulesManager::Level::kAllow;
    bool is_restricted_for_destination = false;
    // Only if it's restricted by any rule and the destination is passed, check
    // if this combination is also blocked or not.
    if (level == DlpRulesManager::Level::kBlock && destination.has_value()) {
      std::optional<data_controls::Component> dst_component =
          destination->component();
      if (dst_component.has_value()) {
        DlpRulesManager::Level dst_level =
            rules_manager_->IsRestrictedComponent(
                GURL(metadata.source_url()), dst_component.value(),
                DlpRulesManager::Restriction::kFiles, nullptr, nullptr);
        is_restricted_for_destination =
            dst_level == DlpRulesManager::Level::kBlock;
      } else {
        DCHECK(destination->url());
        DlpRulesManager::Level dst_level =
            rules_manager_->IsRestrictedDestination(
                GURL(metadata.source_url()), *destination->url(),
                DlpRulesManager::Restriction::kFiles, nullptr, nullptr,
                nullptr);
        is_restricted_for_destination =
            dst_level == DlpRulesManager::Level::kBlock;
      }
    }

    metadata_map.emplace(
        metadata.path(),
        DlpFileMetadata(metadata.source_url(), metadata.referrer_url(),
                        is_dlp_restricted, is_restricted_for_destination));
  }

  std::vector<DlpFileMetadata> result;
  for (const auto& file : files) {
    auto metadata_itr = metadata_map.find(file.path().value());
    if (metadata_itr == metadata_map.end()) {
      result.emplace_back("", "", false, false);
    } else {
      result.emplace_back(metadata_itr->second);
    }
  }

  std::move(result_callback).Run(std::move(result));
}

void DlpFilesControllerAsh::MaybeReportEvent(
    ino64_t inode,
    time_t crtime,
    const base::FilePath& path,
    const std::string& source_url,
    const DlpFileDestination& dst,
    const std::optional<std::string>& dst_pattern,
    const DlpRulesManager::RuleMetadata& rule_metadata,
    std::optional<DlpRulesManager::Level> level) {
  const bool is_warning_proceeded_event = !level.has_value();

  if (!is_warning_proceeded_event &&
      (level.value() == DlpRulesManager::Level::kAllow ||
       level.value() == DlpRulesManager::Level::kNotSet)) {
    return;
  }

  data_controls::DlpReportingManager* reporting_manager =
      rules_manager_->GetReportingManager();
  if (!reporting_manager) {
    return;
  }

  // Warning proceeded events are always user-initiated since they are triggered
  // only when the user interacts with the warning dialog.
  if (!is_warning_proceeded_event &&
      !event_storage_->StoreEventAndCheckIfItShouldBeReported({inode, crtime},
                                                              dst)) {
    return;
  }

  std::unique_ptr<data_controls::DlpPolicyEventBuilder> event_builder =
      is_warning_proceeded_event
          ? data_controls::DlpPolicyEventBuilder::WarningProceededEvent(
                source_url, rule_metadata.name, rule_metadata.obfuscated_id,
                DlpRulesManager::Restriction::kFiles)
          : data_controls::DlpPolicyEventBuilder::Event(
                source_url, rule_metadata.name, rule_metadata.obfuscated_id,
                DlpRulesManager::Restriction::kFiles, level.value());

  event_builder->SetContentName(path.BaseName().value());

  if (dst_pattern.has_value()) {
    DCHECK(!dst.component().has_value());
    event_builder->SetDestinationUrl(dst_pattern.value());
  } else {
    DCHECK(dst.component().has_value());
    event_builder->SetDestinationComponent(dst.component().value());
  }
  reporting_manager->ReportEvent(event_builder->Create());
}

void DlpFilesControllerAsh::ContinueCheckIfTransferAllowed(
    std::optional<file_manager::io_task::IOTaskId> task_id,
    storage::FileSystemURL destination,
    bool is_move,
    CheckIfTransferAllowedCallback result_callback,
    std::vector<storage::FileSystemURL> transferred_files) {
  if (!chromeos::DlpClient::Get() || !chromeos::DlpClient::Get()->IsAlive()) {
    std::move(result_callback).Run(std::vector<storage::FileSystemURL>());
    return;
  }

  ::dlp::CheckFilesTransferRequest request;
  base::flat_map<std::string, storage::FileSystemURL> transferred_files_map;
  for (const auto& file : transferred_files) {
    auto file_path = file.path().value();
    transferred_files_map[file_path] = file;
    request.add_files_paths(file_path);
  }
  if (transferred_files_map.empty()) {
    std::move(result_callback).Run(std::vector<storage::FileSystemURL>());
    return;
  }

  std::optional<data_controls::Component> component =
      MapFilePathToPolicyComponent(profile_, destination.path());
  ::dlp::DlpComponent proto;
  if (component) {
    proto = dlp::MapPolicyComponentToProto(*component);
  } else {
    proto = ::dlp::DlpComponent::SYSTEM;
  }
  request.set_destination_component(proto);
  request.set_file_action(is_move ? ::dlp::FileAction::MOVE
                                  : ::dlp::FileAction::COPY);
  if (task_id) {
    request.set_io_task_id(task_id.value());
  }

  auto return_transfers_callback =
      base::BindOnce(&DlpFilesControllerAsh::ReturnDisallowedFiles,
                     weak_ptr_factory_.GetWeakPtr(), std::move(task_id),
                     std::move(transferred_files_map),
                     is_move ? dlp::FileAction::kMove : dlp::FileAction::kCopy,
                     std::move(result_callback));
  chromeos::DlpClient::Get()->CheckFilesTransfer(
      request, std::move(return_transfers_callback));
}

void DlpFilesControllerAsh::ContinueFilterDisallowedUploads(
    std::vector<ui::SelectedFileInfo> selected_files,
    const DlpFileDestination& destination,
    FilterDisallowedUploadsCallback result_callback,
    std::vector<storage::FileSystemURL> uploaded_files) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (!chromeos::DlpClient::Get() || !chromeos::DlpClient::Get()->IsAlive()) {
    std::move(result_callback).Run(std::move(selected_files));
    return;
  }

  ::dlp::CheckFilesTransferRequest request;
  for (const auto& file : uploaded_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::UPLOAD);

  auto return_uploads_callback =
      base::BindOnce(&DlpFilesControllerAsh::ReturnAllowedUploads,
                     weak_ptr_factory_.GetWeakPtr(), std::move(selected_files),
                     std::move(result_callback));
  chromeos::DlpClient::Get()->CheckFilesTransfer(
      request, std::move(return_uploads_callback));
}

}  // namespace policy