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

// Copyright 2020 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_rules_manager_impl.h"

#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/values.h"
#include "chrome/browser/ash/policy/dlp/dlp_files_controller_ash.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/policy/dlp/data_transfer_dlp_controller.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_files_controller_lacros.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_scoped_file_access_delegate.h"
#include "chrome/browser/enterprise/data_controls/chrome_dlp_rules_manager.h"
#include "chrome/browser/enterprise/data_controls/dlp_reporting_manager.h"
#include "chrome/common/chrome_features.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/enterprise/data_controls/core/browser/dlp_histogram_helper.h"
#include "components/enterprise/data_controls/core/browser/rule.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/url_matcher/url_util.h"
#include "url/origin.h"

namespace policy {

namespace {

using RuleId = DlpRulesManagerImpl::RuleId;

using UrlConditionId = DlpRulesManagerImpl::UrlConditionId;

using RulesConditionsMap = std::map<RuleId, UrlConditionId>;

constexpr char kDrivePattern[] = "drive.google.com";
constexpr char kOneDrivePattern[] = "onedrive.live.com";

// Creates a condition set for the given `url`.
scoped_refptr<url_matcher::URLMatcherConditionSet> CreateConditionSet(
    url_matcher::URLMatcher* matcher,
    UrlConditionId condition_id,
    const std::string& url) {
  CHECK(matcher);

  std::string scheme;
  std::string host;
  uint16_t port = 0;
  std::string path;
  std::string query;
  bool match_subdomains = true;

  if (!url_matcher::util::FilterToComponents(
          url, &scheme, &host, &match_subdomains, &port, &path, &query)) {
    LOG(ERROR) << "Invalid pattern " << url;
    return nullptr;
  }
  return url_matcher::util::CreateConditionSet(matcher, condition_id, scheme,
                                               host, match_subdomains, port,
                                               path, query, /*allow=*/true);
}

// Creates `urls` conditions, saves patterns strings mapping in
// `patterns_mapping`, and saves conditions ids to rules ids mapping in `map`.
void AddUrlConditions(url_matcher::URLMatcher* matcher,
                      UrlConditionId& condition_id,
                      const base::Value::List* urls,
                      url_matcher::URLMatcherConditionSet::Vector& conditions,
                      std::map<UrlConditionId, std::string>& patterns_mapping,
                      RuleId rule_id,
                      std::map<UrlConditionId, RuleId>& map) {
  DCHECK(urls);
  for (const auto& list_entry : *urls) {
    std::string url = list_entry.GetString();

    ++condition_id;
    auto condition_set = CreateConditionSet(matcher, condition_id, url);
    if (!condition_set) {
      continue;
    }
    conditions.push_back(std::move(condition_set));
    map[condition_id] = rule_id;
    patterns_mapping[condition_id] = url;
  }
}

// Returns the URLs associated with the given component. An empty vector is
// returned if there are none.
std::vector<std::string> GetAssociatedUrlsConditions(
    data_controls::Component component) {
  switch (component) {
    case data_controls::Component::kDrive:
      return {kDrivePattern};
    case data_controls::Component::kOneDrive:
      return {kOneDrivePattern};
    case data_controls::Component::kUnknownComponent:
    case data_controls::Component::kArc:
    case data_controls::Component::kCrostini:
    case data_controls::Component::kPluginVm:
    case data_controls::Component::kUsb:
      return {};
  }
}

// Add URL conditions associated with the given `component`.
void AddAssociatedUrlConditions(
    data_controls::Component component,
    url_matcher::URLMatcher* matcher,
    UrlConditionId& condition_id,
    url_matcher::URLMatcherConditionSet::Vector& conditions,
    std::map<UrlConditionId, std::string>& patterns_mapping,
    RuleId rule_id,
    std::map<UrlConditionId, RuleId>& map) {
  base::Value::List destinations_urls;

  for (const auto& url : GetAssociatedUrlsConditions(component)) {
    destinations_urls.Append(url);
  }

  if (!destinations_urls.empty()) {
    AddUrlConditions(matcher, condition_id, &destinations_urls, conditions,
                     patterns_mapping, rule_id, map);
  }
}

void OnSetDlpFilesPolicy(const ::dlp::SetDlpFilesPolicyResponse response) {
  data_controls::DlpBooleanHistogram(
      data_controls::dlp::kErrorsFilesPolicySetup,
      response.has_error_message());
  if (response.has_error_message()) {
    DlpScopedFileAccessDelegate::DeleteInstance();
    LOG(ERROR) << "Failed to set DLP Files policy and start DLP daemon, error: "
               << response.error_message();
    return;
  }
  DCHECK(chromeos::DlpClient::Get()->IsAlive());
  DlpScopedFileAccessDelegate::Initialize(
      base::BindRepeating(chromeos::DlpClient::Get));
}

::dlp::DlpRuleLevel GetLevelProtoEnum(const DlpRulesManager::Level level) {
  static constexpr auto kLevelsMap =
      base::MakeFixedFlatMap<DlpRulesManager::Level, ::dlp::DlpRuleLevel>(
          {{DlpRulesManager::Level::kNotSet, ::dlp::DlpRuleLevel::UNSPECIFIED},
           {DlpRulesManager::Level::kReport, ::dlp::DlpRuleLevel::UNSPECIFIED},
           {DlpRulesManager::Level::kWarn, ::dlp::DlpRuleLevel::UNSPECIFIED},
           {DlpRulesManager::Level::kBlock, ::dlp::DlpRuleLevel::BLOCK},
           {DlpRulesManager::Level::kAllow, ::dlp::DlpRuleLevel::ALLOW}});
  return kLevelsMap.at(level);
}

}  // namespace

DlpRulesManagerImpl::~DlpRulesManagerImpl() {
  DataTransferDlpController::DeleteInstance();
  DlpScopedFileAccessDelegate::DeleteInstance();
}

// static
void DlpRulesManagerImpl::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(policy_prefs::kDlpReportingEnabled, false);
  registry->RegisterListPref(policy_prefs::kDlpRulesList);
  registry->RegisterIntegerPref(policy_prefs::kDlpClipboardCheckSizeLimit, 0);
}

DlpRulesManager::Level DlpRulesManagerImpl::IsRestrictedComponent(
    const GURL& source,
    const data_controls::Component& destination,
    Restriction restriction,
    std::string* out_source_pattern,
    RuleMetadata* out_rule_metadata) const {
  DCHECK(src_url_matcher_);
  DCHECK(restriction == Restriction::kClipboard ||
         restriction == Restriction::kFiles);

  if (destination == data_controls::Component::kUnknownComponent) {
    return DlpRulesManager::Level::kAllow;
  }

  const RulesConditionsMap src_rules_map = MatchUrlAndGetRulesMapping(
      source, src_url_matcher_.get(), src_url_rules_mapping_);

  auto it = components_rules_.find(destination);
  if (it == components_rules_.end())
    return Level::kAllow;

  const std::set<RuleId>& component_rules_ids = it->second;

  RulesConditionsMap intersection_rules;
  auto src_map_itr = src_rules_map.begin();
  auto component_rules_itr = component_rules_ids.begin();
  while (src_map_itr != src_rules_map.end() &&
         component_rules_itr != component_rules_ids.end()) {
    if (src_map_itr->first < *component_rules_itr) {
      ++src_map_itr;
    } else if (*component_rules_itr < src_map_itr->first) {
      ++component_rules_itr;
    } else {
      intersection_rules.insert(*src_map_itr);
      ++src_map_itr;
      ++component_rules_itr;
    }
  }

  const MatchedRuleInfo rule_info = GetMaxJoinRestrictionLevelAndRuleId(
      restriction, intersection_rules, restrictions_map_);
  if (rule_info.url_condition.has_value() && out_source_pattern) {
    UrlConditionId src_condition_id = rule_info.url_condition.value();
    *out_source_pattern = src_patterns_mapping_.at(src_condition_id);
  }
  if (rule_info.rule_id.has_value() && out_rule_metadata) {
    auto rule_metadata_itr =
        rules_id_metadata_mapping_.find(rule_info.rule_id.value());
    if (rule_metadata_itr != rules_id_metadata_mapping_.end()) {
      *out_rule_metadata = rule_metadata_itr->second;
    }
  }
  return rule_info.level;
}

DlpRulesManager::AggregatedComponents
DlpRulesManagerImpl::GetAggregatedComponents(const GURL& source,
                                             Restriction restriction) const {
  DCHECK(src_url_matcher_);
  DCHECK(restriction == Restriction::kClipboard ||
         restriction == Restriction::kFiles);

  std::map<Level, std::set<data_controls::Component>> result;
  for (data_controls::Component component : data_controls::kAllComponents) {
    std::string out_source_pattern;
    Level level = IsRestrictedComponent(source, component, restriction,
                                        &out_source_pattern, nullptr);
    result[level].insert(component);
  }

  return result;
}

DlpRulesManagerImpl::DlpRulesManagerImpl(PrefService* local_state,
                                         Profile* profile)
    : DlpRulesManager(profile) {
  pref_change_registrar_.Init(local_state);
  pref_change_registrar_.Add(
      policy_prefs::kDlpRulesList,
      base::BindRepeating(&DlpRulesManagerImpl::OnDataLeakPreventionRulesUpdate,
                          base::Unretained(this)));
  OnDataLeakPreventionRulesUpdate();

  if (IsReportingEnabled())
    reporting_manager_ = std::make_unique<data_controls::DlpReportingManager>();

#if BUILDFLAG(IS_CHROMEOS_ASH)
  if (chromeos::DlpClient::Get()) {
    dlp_client_observation_.Observe(chromeos::DlpClient::Get());
  }
#endif
}

bool DlpRulesManagerImpl::IsReportingEnabled() const {
  return g_browser_process->local_state()->GetBoolean(
      policy_prefs::kDlpReportingEnabled);
}

data_controls::DlpReportingManager* DlpRulesManagerImpl::GetReportingManager()
    const {
  return reporting_manager_.get();
}

DlpFilesController* DlpRulesManagerImpl::GetDlpFilesController() const {
  return files_controller_.get();
}

size_t DlpRulesManagerImpl::GetClipboardCheckSizeLimitInBytes() const {
  return pref_change_registrar_.prefs()->GetInteger(
      policy_prefs::kDlpClipboardCheckSizeLimit);
}

bool DlpRulesManagerImpl::IsFilesPolicyEnabled() const {
  return base::FeatureList::IsEnabled(
             features::kDataLeakPreventionFilesRestriction) &&
         base::Contains(restrictions_map_,
                        DlpRulesManager::Restriction::kFiles) &&
         chromeos::DlpClient::Get() && chromeos::DlpClient::Get()->IsAlive();
}

void DlpRulesManagerImpl::DlpDaemonRestarted() {
  // This should trigger re-notification of DLP daemon if needed.
  OnDataLeakPreventionRulesUpdate();
}

void DlpRulesManagerImpl::Shutdown() {
  // There are FilesController implementations such as DlpFilesControllerAsh
  // that are using the Profile to do some cleanup (e.g., stop observing the
  // VolumeManager). This cleanup must be done when the KeyedService::Shutdown
  // method is called.
  files_controller_.reset();
}

void DlpRulesManagerImpl::OnDataLeakPreventionRulesUpdate() {
  components_rules_.clear();
  restrictions_map_.clear();
  src_url_rules_mapping_.clear();
  dst_url_rules_mapping_.clear();
  src_url_matcher_ = std::make_unique<url_matcher::URLMatcher>();
  dst_url_matcher_ = std::make_unique<url_matcher::URLMatcher>();
  src_patterns_mapping_.clear();
  dst_patterns_mapping_.clear();
  src_conditions_.clear();
  dst_conditions_.clear();
  rules_id_metadata_mapping_.clear();
#if BUILDFLAG(IS_CHROMEOS_ASH) || BUILDFLAG(IS_CHROMEOS_LACROS)
  files_controller_ = nullptr;
#endif

  if (!base::FeatureList::IsEnabled(features::kDataLeakPreventionPolicy)) {
    return;
  }

  const base::Value::List& rules_list =
      g_browser_process->local_state()->GetList(policy_prefs::kDlpRulesList);

  data_controls::DlpBooleanHistogram(data_controls::dlp::kDlpPolicyPresentUMA,
                                     !rules_list.empty());
  if (rules_list.empty()) {
    DataTransferDlpController::DeleteInstance();
    return;
  }

  RuleId rules_counter = 0;
  UrlConditionId src_url_condition_id = 0;
  UrlConditionId dst_url_condition_id = 0;

  // Constructing request to send the policy to DLP Files daemon.
  ::dlp::SetDlpFilesPolicyRequest request_to_daemon;

  for (const base::Value& rule_value : rules_list) {
    const base::Value::Dict& rule = rule_value.GetDict();
    const base::Value::Dict* sources = rule.FindDict("sources");
    DCHECK(sources);
    const base::Value::List* sources_urls = sources->FindList("urls");
    DCHECK(sources_urls);  // This DCHECK should be removed when other types are
                           // supported as sources.

    AddUrlConditions(src_url_matcher_.get(), src_url_condition_id, sources_urls,
                     src_conditions_, src_patterns_mapping_, rules_counter,
                     src_url_rules_mapping_);

    const base::Value::Dict* destinations = rule.FindDict("destinations");
    const base::Value::List* destinations_urls =
        destinations ? destinations->FindList("urls") : nullptr;
    if (destinations_urls) {
      AddUrlConditions(dst_url_matcher_.get(), dst_url_condition_id,
                       destinations_urls, dst_conditions_,
                       dst_patterns_mapping_, rules_counter,
                       dst_url_rules_mapping_);
    }
    const base::Value::List* destinations_components =
        destinations ? destinations->FindList("components") : nullptr;
    if (destinations_components) {
      for (const auto& component : *destinations_components) {
        DCHECK(component.is_string());
        data_controls::Component component_mapping =
            data_controls::GetComponentMapping(component.GetString());
        components_rules_[component_mapping].insert(rules_counter);
        AddAssociatedUrlConditions(component_mapping, dst_url_matcher_.get(),
                                   dst_url_condition_id, dst_conditions_,
                                   dst_patterns_mapping_, rules_counter,
                                   dst_url_rules_mapping_);
      }
    }

    const std::string* rule_name = rule.FindString("name");
    const std::string* rule_id = rule.FindString("rule_id");
    // Only add to metadata if both fields are set, so we can control behaviour
    // from the server side.
    if (rule_name && rule_id) {
      rules_id_metadata_mapping_.emplace(rules_counter,
                                         RuleMetadata(*rule_name, *rule_id));
    }

    const base::Value::List* restrictions = rule.FindList("restrictions");
    DCHECK(restrictions);
    for (const auto& restriction_value : *restrictions) {
      const base::Value::Dict& restriction = restriction_value.GetDict();
      const std::string* rule_class_str = restriction.FindString("class");
      DCHECK(rule_class_str);
      const std::string* rule_level_str = restriction.FindString("level");
      DCHECK(rule_level_str);

      const Restriction rule_restriction =
          data_controls::Rule::StringToRestriction(*rule_class_str);
      if (rule_restriction == Restriction::kUnknownRestriction)
        continue;

      Level rule_level = data_controls::Rule::StringToLevel(*rule_level_str);
      if (rule_level == Level::kNotSet)
        continue;

      bool rule_has_destinations =
          destinations_urls && !destinations_urls->empty();
      bool rule_has_components =
          destinations_components && !destinations_components->empty();

      if (rule_restriction == Restriction::kFiles &&
          (rule_has_destinations || rule_has_components)) {
        ::dlp::DlpFilesRule files_rule;
        for (const auto& url : *sources_urls) {
          DCHECK(url.is_string());
          files_rule.add_source_urls(url.GetString());
        }

        if (rule_has_destinations) {
          for (const auto& url : *destinations_urls) {
            DCHECK(url.is_string());
            files_rule.add_destination_urls(url.GetString());
          }
        }

        if (rule_has_components) {
          for (const auto& component : *destinations_components) {
            DCHECK(component.is_string());
            files_rule.add_destination_components(
                data_controls::GetComponentProtoMapping(component.GetString()));
            for (const auto& url :
                 GetAssociatedUrlsConditions(data_controls::GetComponentMapping(
                     component.GetString()))) {
              files_rule.add_destination_urls(url);
            }
          }
        }

        files_rule.set_level(GetLevelProtoEnum(rule_level));
        request_to_daemon.mutable_rules()->Add(std::move(files_rule));
      }

      DlpRestrictionConfiguredHistogram(rule_restriction);
      restrictions_map_[rule_restriction].emplace(rules_counter, rule_level);
    }
    ++rules_counter;
  }

  src_url_matcher_->AddConditionSets(src_conditions_);
  dst_url_matcher_->AddConditionSets(dst_conditions_);
  if (base::Contains(restrictions_map_, Restriction::kClipboard)
      || (base::FeatureList::IsEnabled(
              features::kDataLeakPreventionFilesRestriction) &&
          request_to_daemon.rules_size() > 0)
  ) {
    DataTransferDlpController::Init(*this);
  } else {
    DataTransferDlpController::DeleteInstance();
  }

  if (base::FeatureList::IsEnabled(
          features::kDataLeakPreventionFilesRestriction)) {
    if (request_to_daemon.rules_size() > 0) {
      // Start and/or activate the daemon.
      data_controls::DlpBooleanHistogram(
          data_controls::dlp::kFilesDaemonStartedUMA, true);
      chromeos::DlpClient::Get()->SetDlpFilesPolicy(
          request_to_daemon, base::BindOnce(&OnSetDlpFilesPolicy));
#if BUILDFLAG(IS_CHROMEOS_ASH)
      if (!files_controller_) {
        files_controller_ =
            std::make_unique<DlpFilesControllerAsh>(*this, profile_);
      }
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
      if (!files_controller_) {
        files_controller_ = std::make_unique<DlpFilesControllerLacros>(*this);
      }
#endif
    } else if (chromeos::DlpClient::Get() &&
               chromeos::DlpClient::Get()->IsAlive()) {
      // The daemon is running, but should be deactivated by sending empty
      // policy.
      chromeos::DlpClient::Get()->SetDlpFilesPolicy(
          request_to_daemon, base::BindOnce(&OnSetDlpFilesPolicy));
    } else {
      // The daemon is not running and should not be communicated.
      DlpScopedFileAccessDelegate::DeleteInstance();
    }
  }
}

}  // namespace policy