chromium/chrome/browser/ash/dbus/chrome_features_service_provider.cc

// Copyright 2018 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/dbus/chrome_features_service_provider.h"

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

#include "ash/components/arc/arc_features.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_pref_names.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_features.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/chrome_features.h"
#include "chromeos/ash/components/install_attributes/install_attributes.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "components/prefs/pref_service.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash {

namespace {

// A prefix to apply to all features which Chrome OS platform-side code wishes
// to check.
// This prefix must *only* be applied to features on the platform side, and no
// |base::Feature|s should be defined with this prefix.
// A presubmit will enforce that no |base::Feature|s will be defined with this
// prefix.
// TODO(crbug.com/40202807): Add the aforementioned presubmit.
constexpr char kCrOSLateBootFeaturePrefix[] = "CrOSLateBoot";

void SendResponse(dbus::MethodCall* method_call,
                  dbus::ExportedObject::ResponseSender response_sender,
                  bool answer,
                  const std::string& reason = std::string()) {
  std::unique_ptr<dbus::Response> response =
      dbus::Response::FromMethodCall(method_call);
  dbus::MessageWriter writer(response.get());
  writer.AppendBool(answer);
  if (!reason.empty())
    writer.AppendString(reason);
  std::move(response_sender).Run(std::move(response));
}

Profile* GetSenderProfile(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender* response_sender) {
  dbus::MessageReader reader(method_call);
  std::string user_id_hash;

  if (!reader.PopString(&user_id_hash)) {
    LOG(ERROR) << "Failed to pop user_id_hash from incoming message.";
    std::move(*response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(method_call,
                                                 DBUS_ERROR_INVALID_ARGS,
                                                 "No user_id_hash string arg"));
    return nullptr;
  }

  if (user_id_hash.empty())
    return ProfileManager::GetActiveUserProfile();

  return g_browser_process->profile_manager()->GetProfileByPath(
      ProfileHelper::GetProfilePathByUserIdHash(user_id_hash));
}

}  // namespace

ChromeFeaturesServiceProvider::ChromeFeaturesServiceProvider(
    std::unique_ptr<base::FeatureList::Accessor> feature_list_accessor)
    : feature_list_accessor_(std::move(feature_list_accessor)) {}

ChromeFeaturesServiceProvider::~ChromeFeaturesServiceProvider() = default;

void ChromeFeaturesServiceProvider::Start(
    scoped_refptr<dbus::ExportedObject> exported_object) {
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceIsFeatureEnabledMethod,
      base::BindRepeating(&ChromeFeaturesServiceProvider::IsFeatureEnabled,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&ChromeFeaturesServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceGetFeatureParamsMethod,
      base::BindRepeating(&ChromeFeaturesServiceProvider::GetFeatureParams,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&ChromeFeaturesServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceIsCrostiniEnabledMethod,
      base::BindRepeating(&ChromeFeaturesServiceProvider::IsCrostiniEnabled,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&ChromeFeaturesServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceIsPluginVmEnabledMethod,
      base::BindRepeating(&ChromeFeaturesServiceProvider::IsPluginVmEnabled,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&ChromeFeaturesServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceIsCryptohomeDistributedModelEnabledMethod,
      base::BindRepeating(
          &ChromeFeaturesServiceProvider::IsCryptohomeDistributedModelEnabled,
          weak_ptr_factory_.GetWeakPtr()),
      base::BindRepeating(&ChromeFeaturesServiceProvider::OnExported,
                          weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceIsCryptohomeUserDataAuthEnabledMethod,
      base::BindRepeating(
          &ChromeFeaturesServiceProvider::IsCryptohomeUserDataAuthEnabled,
          weak_ptr_factory_.GetWeakPtr()),
      base::BindRepeating(&ChromeFeaturesServiceProvider::OnExported,
                          weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::
          kChromeFeaturesServiceIsCryptohomeUserDataAuthKillswitchEnabledMethod,
      base::BindRepeating(&ChromeFeaturesServiceProvider::
                              IsCryptohomeUserDataAuthKillswitchEnabled,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindRepeating(&ChromeFeaturesServiceProvider::OnExported,
                          weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceIsVmManagementCliAllowedMethod,
      base::BindRepeating(
          &ChromeFeaturesServiceProvider::IsVmManagementCliAllowed,
          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&ChromeFeaturesServiceProvider::OnExported,
                     weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceIsPeripheralDataAccessEnabledMethod,
      base::BindRepeating(
          &ChromeFeaturesServiceProvider::IsPeripheralDataAccessEnabled,
          weak_ptr_factory_.GetWeakPtr()),
      base::BindRepeating(&ChromeFeaturesServiceProvider::OnExported,
                          weak_ptr_factory_.GetWeakPtr()));
  exported_object->ExportMethod(
      chromeos::kChromeFeaturesServiceInterface,
      chromeos::kChromeFeaturesServiceIsDNSProxyEnabledMethod,
      base::BindRepeating(&ChromeFeaturesServiceProvider::IsDnsProxyEnabled,
                          weak_ptr_factory_.GetWeakPtr()),
      base::BindRepeating(&ChromeFeaturesServiceProvider::OnExported,
                          weak_ptr_factory_.GetWeakPtr()));
}

void ChromeFeaturesServiceProvider::OnExported(
    const std::string& interface_name,
    const std::string& method_name,
    bool success) {
  if (!success) {
    LOG(ERROR) << "Failed to export " << interface_name << "." << method_name;
  }
}

void ChromeFeaturesServiceProvider::IsFeatureEnabled(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  static const base::Feature constexpr* kFeatureLookup[] = {
      &arc::kBootCompletedBroadcastFeature,
      &arc::kCustomTabsExperimentFeature,
      &arc::kFilePickerExperimentFeature,
      &arc::kNativeBridgeToggleFeature,
      &features::kSessionManagerLongKillTimeout,
      &features::kSessionManagerLivenessCheck,
      &features::kBorealisProvision,
      &features::kDeferConciergeStartup,
  };

  dbus::MessageReader reader(method_call);
  std::string feature_name;
  if (!reader.PopString(&feature_name)) {
    LOG(ERROR) << "Failed to pop feature_name from incoming message.";
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(
            method_call, DBUS_ERROR_INVALID_ARGS,
            "Missing or invalid feature_name string arg."));
    return;
  }

  auto* const* it =
      base::ranges::find(kFeatureLookup, feature_name, &base::Feature::name);
  if (it != std::end(kFeatureLookup)) {
    SendResponse(method_call, std::move(response_sender),
                 base::FeatureList::IsEnabled(**it));
    return;
  }
  // Not on our list. Potentially look up by name instead.
  // Only search for arbitrary trial names that begin with the appropriate
  // prefix, since looking up a feature by name will not be able to get the
  // default value associated with any `base::Feature` defined in the code
  // base.
  // Separately, a presubmit will enforce that no `base::Feature` definition
  // has a name starting with this prefix.
  // TODO(crbug.com/40202807): Add the aforementioned presubmit.
  base::FeatureList::OverrideState state =
      base::FeatureList::OVERRIDE_USE_DEFAULT;
  if (feature_name.find(kCrOSLateBootFeaturePrefix) == 0) {
    state = feature_list_accessor_->GetOverrideStateByFeatureName(feature_name);
  } else {
    LOG(ERROR) << "Invalid prefix on feature " << feature_name << " (want "
               << kCrOSLateBootFeaturePrefix << ")";
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(
            method_call, DBUS_ERROR_INVALID_ARGS,
            base::StrCat({"Invalid prefix for feature name: '", feature_name,
                          "'. Want ", kCrOSLateBootFeaturePrefix})));
    return;
  }
  if (state == base::FeatureList::OVERRIDE_USE_DEFAULT) {
    VLOG(1) << "Unexpected feature name '" << feature_name << "'"
            << " (likely just indicates there isn't a variations seed).";
    // This isn't really an error, we're just using the error channel to signal
    // to feature_library that it should fall back to its defaults.
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(
            method_call, DBUS_ERROR_INVALID_ARGS,
            base::StrCat({"Chrome can't get state for '", feature_name,
                          "'; feature_library will decide"})));
    return;
  }
  SendResponse(method_call, std::move(response_sender),
               state == base::FeatureList::OVERRIDE_ENABLE_FEATURE);
}

void ChromeFeaturesServiceProvider::GetFeatureParams(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  dbus::MessageReader reader(method_call);
  dbus::MessageReader array_reader(nullptr);
  if (!reader.PopArray(&array_reader)) {
    LOG(ERROR) << "Failed to read array of feature names.";
    std::move(response_sender)
        .Run(dbus::ErrorResponse::FromMethodCall(
            method_call, DBUS_ERROR_INVALID_ARGS,
            "Could not pop string array of feature names"));
    return;
  }

  std::vector<std::string> features;
  std::map<std::string, std::map<std::string, std::string>> params_map;
  std::map<std::string, bool> enabled_map;
  while (array_reader.HasMoreData()) {
    std::string feature_name;

    if (!array_reader.PopString(&feature_name)) {
      LOG(ERROR) << "Failed to pop feature_name from array.";
      std::move(response_sender)
          .Run(dbus::ErrorResponse::FromMethodCall(
              method_call, DBUS_ERROR_INVALID_ARGS,
              "Missing or invalid feature_name string arg in array."));
      return;
    }

    if (feature_name.find(kCrOSLateBootFeaturePrefix) != 0) {
      LOG(ERROR) << "Unexpected prefix on feature name '" << feature_name << "'"
                 << " (want " << kCrOSLateBootFeaturePrefix << ")";
      std::move(response_sender)
          .Run(dbus::ErrorResponse::FromMethodCall(
              method_call, DBUS_ERROR_INVALID_ARGS,
              base::StrCat({"Invalid prefix for feature name: '", feature_name,
                            "'. Want ", kCrOSLateBootFeaturePrefix})));
      return;
    }

    features.push_back(feature_name);

    base::FeatureList::OverrideState state =
        feature_list_accessor_->GetOverrideStateByFeatureName(feature_name);
    if (state == base::FeatureList::OVERRIDE_ENABLE_FEATURE) {
      enabled_map[feature_name] = true;
    } else if (state == base::FeatureList::OVERRIDE_DISABLE_FEATURE) {
      enabled_map[feature_name] = false;
    }
    // else leave it out of the map.

    std::map<std::string, std::string> per_feature_map;
    if (!feature_list_accessor_->GetParamsByFeatureName(feature_name,
                                                        &per_feature_map)) {
      VLOG(1) << "No trial found for '" << feature_name << "', skipping."
              << " (likely just means there is no variations seed)";
      continue;
    }
    params_map[feature_name] = std::move(per_feature_map);
  }

  // Build response
  std::unique_ptr<dbus::Response> response =
      dbus::Response::FromMethodCall(method_call);
  dbus::MessageWriter writer(response.get());
  dbus::MessageWriter array_writer(nullptr);
  // A map from feature name to:
  // * two booleans:
  //   * Whether to use the override (or the default),
  //   * What the override state is (only valid if we should use the
  //     override value).
  // * Another map, from parameter name to value.
  writer.OpenArray("{s(bba{ss})}", &array_writer);
  for (const auto& feature_name : features) {
    dbus::MessageWriter feature_dict_writer(nullptr);
    array_writer.OpenDictEntry(&feature_dict_writer);
    feature_dict_writer.AppendString(feature_name);
    dbus::MessageWriter struct_writer(nullptr);
    feature_dict_writer.OpenStruct(&struct_writer);

    if (enabled_map.find(feature_name) != enabled_map.end()) {
      struct_writer.AppendBool(true);  // Use override
      struct_writer.AppendBool(enabled_map[feature_name]);
    } else {
      struct_writer.AppendBool(false);  // Ignore override
      struct_writer.AppendBool(false);  // Arbitrary choice
    }

    dbus::MessageWriter sub_array_writer(nullptr);
    struct_writer.OpenArray("{ss}", &sub_array_writer);
    if (params_map.find(feature_name) != params_map.end()) {
      const auto& submap = params_map[feature_name];
      for (const auto& [key, value] : submap) {
        dbus::MessageWriter dict_writer(nullptr);
        sub_array_writer.OpenDictEntry(&dict_writer);
        dict_writer.AppendString(key);
        dict_writer.AppendString(value);
        sub_array_writer.CloseContainer(&dict_writer);
      }
    }
    struct_writer.CloseContainer(&sub_array_writer);
    feature_dict_writer.CloseContainer(&struct_writer);
    array_writer.CloseContainer(&feature_dict_writer);
  }
  writer.CloseContainer(&array_writer);
  std::move(response_sender).Run(std::move(response));
}

void ChromeFeaturesServiceProvider::IsCrostiniEnabled(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  Profile* profile = GetSenderProfile(method_call, &response_sender);
  if (!profile)
    return;

  std::string reason;
  bool answer =
      crostini::CrostiniFeatures::Get()->IsAllowedNow(profile, &reason);
  SendResponse(method_call, std::move(response_sender), answer, reason);
}

void ChromeFeaturesServiceProvider::IsCryptohomeDistributedModelEnabled(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  SendResponse(
      method_call, std::move(response_sender),
      base::FeatureList::IsEnabled(::features::kCryptohomeDistributedModel));
}

void ChromeFeaturesServiceProvider::IsCryptohomeUserDataAuthEnabled(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  SendResponse(
      method_call, std::move(response_sender),
      base::FeatureList::IsEnabled(::features::kCryptohomeUserDataAuth));
}

void ChromeFeaturesServiceProvider::IsCryptohomeUserDataAuthKillswitchEnabled(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  SendResponse(method_call, std::move(response_sender),
               base::FeatureList::IsEnabled(
                   ::features::kCryptohomeUserDataAuthKillswitch));
}

void ChromeFeaturesServiceProvider::IsPluginVmEnabled(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  Profile* profile = GetSenderProfile(method_call, &response_sender);
  if (!profile)
    return;

  std::string reason;
  bool answer = plugin_vm::PluginVmFeatures::Get()->IsAllowed(profile, &reason);
  SendResponse(method_call, std::move(response_sender), answer, reason);
}

void ChromeFeaturesServiceProvider::IsVmManagementCliAllowed(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  Profile* profile = GetSenderProfile(method_call, &response_sender);
  if (!profile)
    return;

  SendResponse(method_call, std::move(response_sender),
               profile->GetPrefs()->GetBoolean(
                   crostini::prefs::kVmManagementCliAllowedByPolicy));
}

void ChromeFeaturesServiceProvider::IsPeripheralDataAccessEnabled(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  // TODO(1242686): Add end-to-end tests for this D-Bus signal.

  bool peripheral_data_access_enabled = false;
  // Enterprise managed devices use the local state pref.
  if (InstallAttributes::Get()->IsEnterpriseManaged()) {
    peripheral_data_access_enabled =
        g_browser_process->local_state()->GetBoolean(
            prefs::kLocalStateDevicePeripheralDataAccessEnabled);
  } else {
    // Consumer devices use the CrosSetting pref.
    CrosSettings::Get()->GetBoolean(kDevicePeripheralDataAccessEnabled,
                                    &peripheral_data_access_enabled);
  }
  SendResponse(method_call, std::move(response_sender),
               peripheral_data_access_enabled);
}

void ChromeFeaturesServiceProvider::IsDnsProxyEnabled(
    dbus::MethodCall* method_call,
    dbus::ExportedObject::ResponseSender response_sender) {
  SendResponse(method_call, std::move(response_sender),
               !base::FeatureList::IsEnabled(features::kDisableDnsProxy));
}

}  // namespace ash