chromium/chromeos/ash/components/network/onc/onc_merger.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/network/onc/onc_merger.h"

#include <array>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>

#include "base/check.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/values.h"
#include "chromeos/ash/components/network/policy_util.h"
#include "chromeos/components/onc/onc_signature.h"
#include "components/onc/onc_constants.h"

namespace ash::onc {
namespace {

// Returns true if the field is the identifier of a configuration, i.e. the GUID
// of a network or a certificate, the ICCID of a cellular.
bool IsIdentifierField(const chromeos::onc::OncValueSignature& value_signature,
                       const std::string& field_name) {
  if (&value_signature == &chromeos::onc::kNetworkConfigurationSignature) {
    return field_name == ::onc::network_config::kGUID;
  }
  if (&value_signature == &chromeos::onc::kCertificateSignature) {
    return field_name == ::onc::certificate::kGUID;
  }
  if (&value_signature == &chromeos::onc::kCellularSignature) {
    return field_name == ::onc::cellular::kICCID;
  }
  return false;
}

// Identifier fields and other read-only fields (specifically Type) are
// handled specially during merging because they are always identical for the
// various setting sources.
bool IsReadOnlyField(const chromeos::onc::OncValueSignature& value_signature,
                     const std::string& field_name) {
  if (IsIdentifierField(value_signature, field_name)) {
    return true;
  }
  if (&value_signature == &chromeos::onc::kNetworkConfigurationSignature) {
    return field_name == ::onc::network_config::kType;
  }
  return false;
}

// Inserts |true| at every field name in |result| that is recommended in
// |policy|.
void MarkRecommendedFieldnames(const base::Value::Dict& policy,
                               base::Value::Dict* result) {
  const base::Value::List* recommended_value =
      policy.FindList(::onc::kRecommended);
  if (!recommended_value) {
    return;
  }
  for (const auto& value : *recommended_value) {
    if (value.is_string()) {
      result->Set(value.GetString(), true);
    }
  }
}

// Returns the default value for ONC field specified by |field|.
base::Value GetDefaultValue(const chromeos::onc::OncFieldSignature* field) {
  if (field->default_value_setter) {
    return field->default_value_setter();
  }

  DCHECK(field->value_signature);

  // Return the default base::Value for the field type.
  return base::Value(field->value_signature->onc_type);
}

// Returns a dictionary which contains |true| at each path that is editable by
// the user. No other fields are set.
base::Value::Dict GetEditableFlags(const base::Value::Dict& policy) {
  base::Value::Dict result;
  MarkRecommendedFieldnames(policy, &result);

  // Recurse into nested dictionaries.
  for (auto iter : policy) {
    if (iter.first == ::onc::kRecommended || !iter.second.is_dict()) {
      continue;
    }
    result.Set(iter.first, GetEditableFlags(iter.second.GetDict()));
  }
  return result;
}

// If `dict` doesn't have key `key` yet, set it to `value`.
template <typename ValueType>
void SetIfNotSet(base::Value::Dict& dict,
                 std::string_view key,
                 ValueType value) {
  if (dict.Find(key)) {
    return;
  }
  dict.Set(key, std::move(value));
}

// This is the base class for merging a list of Values in parallel. See
// MergeDictionaries function.
class MergeListOfDictionaries {
 public:
  enum {
    // The policies and settings that are being merged.
    kUserPolicyIndex,
    kDevicePolicyIndex,
    kUserSettingsIndex,
    kSharedSettingsIndex,
    kActiveSettingsIndex,
    // Bool values indicating whether the policies/settings are editable.
    kUserEditableIndex,
    kDeviceEditableIndex,
    kLength,
  };
  using DictPointers = std::array<const base::Value::Dict*, kLength>;
  using ValuePointers = std::array<const base::Value*, kLength>;

  MergeListOfDictionaries() = default;
  virtual ~MergeListOfDictionaries() = default;

  MergeListOfDictionaries(const MergeListOfDictionaries&) = delete;
  MergeListOfDictionaries& operator=(const MergeListOfDictionaries&) = delete;

  // For each path in any of the dictionaries |dicts|, either MergeListOfValues
  // or MergeNestedDictionaries is called with the list of values that are
  // located at that path in each of the dictionaries. This function returns a
  // new dictionary containing all results of those calls at the respective
  // paths. The resulting dictionary doesn't contain empty dictionaries.
  base::Value::Dict MergeDictionaries(const DictPointers& dicts) {
    base::Value::Dict result;
    std::set<std::string> visited;
    for (const base::Value::Dict* dict_outer : dicts) {
      if (!dict_outer) {
        continue;
      }

      for (auto field : *dict_outer) {
        const std::string& key = field.first;
        if (key == ::onc::kRecommended || !visited.insert(key).second) {
          continue;
        }

        // Merge as dictionaries or values depending on the type of the value.
        if (field.second.is_dict()) {
          DictPointers dicts_for_key = {0};
          for (size_t i = 0; i < dicts.size(); ++i) {
            dicts_for_key[i] = dicts[i] ? dicts[i]->FindDict(key) : nullptr;
          }

          base::Value::Dict merged_dict =
              MergeNestedDictionaries(key, dicts_for_key);
          if (!merged_dict.empty()) {
            result.Set(key, std::move(merged_dict));
          }
        } else {
          ValuePointers values_for_key = {0};
          for (size_t i = 0; i < dicts.size(); ++i) {
            values_for_key[i] = dicts[i] ? dicts[i]->Find(key) : nullptr;
          }

          base::Value merged_value = MergeListOfValues(key, values_for_key);
          if (!merged_value.is_none()) {
            result.Set(key, std::move(merged_value));
          }
        }
      }
    }
    return result;
  }

 protected:
  // This function is called by MergeDictionaries for each list of values that
  // are located at the same path in each of the dictionaries. If a dictionary
  // doesn't contain a path then its value is nullptr.
  virtual base::Value MergeListOfValues(const std::string& key,
                                        const ValuePointers& values) = 0;

  virtual base::Value::Dict MergeNestedDictionaries(const std::string& key,
                                                    const DictPointers& dicts) {
    return MergeDictionaries(dicts);
  }
};

// This is the base class for merging policies and user settings.
class MergeSettingsAndPolicies : public MergeListOfDictionaries {
 public:
  struct ValueParams {
    raw_ptr<const base::Value> user_policy;
    raw_ptr<const base::Value> device_policy;
    raw_ptr<const base::Value> user_setting;
    raw_ptr<const base::Value> shared_setting;
    raw_ptr<const base::Value> active_setting;
    bool user_editable;
    bool device_editable;
  };

  MergeSettingsAndPolicies() = default;

  MergeSettingsAndPolicies(const MergeSettingsAndPolicies&) = delete;
  MergeSettingsAndPolicies& operator=(const MergeSettingsAndPolicies&) = delete;

  // Merge the provided dictionaries. For each path in any of the dictionaries,
  // MergeValues is called. Its results are collected in a new dictionary which
  // is then returned. The resulting dictionary never contains empty
  // dictionaries.
  base::Value::Dict MergeDictionaries(
      const base::Value::Dict* user_policy,
      const base::Value::Dict* device_policy,
      const base::Value::Dict* user_settings,
      const base::Value::Dict* shared_settings,
      const base::Value::Dict* active_settings) {
    has_user_policy_ = (user_policy != nullptr);
    has_device_policy_ = (device_policy != nullptr);

    base::Value::Dict user_editable;
    if (user_policy) {
      user_editable = GetEditableFlags(*user_policy);
    }

    base::Value::Dict device_editable;
    if (device_policy) {
      device_editable = GetEditableFlags(*device_policy);
    }

    DictPointers dicts;
    dicts[kUserPolicyIndex] = user_policy;
    dicts[kDevicePolicyIndex] = device_policy;
    dicts[kUserSettingsIndex] = user_settings;
    dicts[kSharedSettingsIndex] = shared_settings;
    dicts[kActiveSettingsIndex] = active_settings;
    dicts[kUserEditableIndex] = &user_editable;
    dicts[kDeviceEditableIndex] = &device_editable;
    return MergeListOfDictionaries::MergeDictionaries(dicts);
  }

 protected:
  // This function is called by MergeDictionaries for each list of values that
  // are located at the same path in each of the dictionaries. Implementations
  // can use the Has*Policy functions.
  virtual base::Value MergeValues(const std::string& key,
                                  const ValueParams& values) = 0;

  // Whether a user policy was provided.
  bool HasUserPolicy() { return has_user_policy_; }

  // Whether a device policy was provided.
  bool HasDevicePolicy() { return has_device_policy_; }

  // MergeListOfDictionaries override.
  base::Value MergeListOfValues(const std::string& key,
                                const ValuePointers& values) override {
    bool user_editable = !HasUserPolicy();
    if (values[kUserEditableIndex] && values[kUserEditableIndex]->is_bool()) {
      user_editable = values[kUserEditableIndex]->GetBool();
    }

    bool device_editable = !HasDevicePolicy();
    if (values[kDeviceEditableIndex] &&
        values[kDeviceEditableIndex]->is_bool()) {
      device_editable = values[kDeviceEditableIndex]->GetBool();
    }

    ValueParams params;
    params.user_policy = values[kUserPolicyIndex];
    params.device_policy = values[kDevicePolicyIndex];
    params.user_setting = values[kUserSettingsIndex];
    params.shared_setting = values[kSharedSettingsIndex];
    params.active_setting = values[kActiveSettingsIndex];
    params.user_editable = user_editable;
    params.device_editable = device_editable;
    return MergeValues(key, params);
  }

 private:
  bool has_user_policy_;
  bool has_device_policy_;
};

// Call MergeDictionaries to merge policies and settings to the effective
// values. This ignores the active settings of Shill. See the description of
// MergeSettingsAndPoliciesToEffective.
class MergeToEffective : public MergeSettingsAndPolicies {
 public:
  MergeToEffective() = default;

  MergeToEffective(const MergeToEffective&) = delete;
  MergeToEffective& operator=(const MergeToEffective&) = delete;

 protected:
  // Merges |values| to the effective value (mandatory policy overwrites user
  // settings overwrites shared settings overwrites recommended policy). Returns
  // the effective value, as well as the respective onc::kAugmentation* constant
  // that indicates which source of settings is effective. Note that this
  // function may return nullopt and ::onc::kAugmentationUserPolicy, which means
  // that the user policy didn't set a value but also didn't recommend it, thus
  // enforcing the empty value.
  struct MergeResult {
    base::Value effective_value;
    const char* effective_source = nullptr;
  };
  MergeResult MergeValuesImpl(const std::string& key,
                              const ValueParams& values) {
    const base::Value* value = nullptr;
    MergeResult result;
    if (!values.user_editable) {
      value = values.user_policy;
      result.effective_source = ::onc::kAugmentationUserPolicy;
    } else if (!values.device_editable) {
      value = values.device_policy;
      result.effective_source = ::onc::kAugmentationDevicePolicy;
    } else if (values.user_setting) {
      value = values.user_setting;
      result.effective_source = ::onc::kAugmentationUserSetting;
    } else if (values.shared_setting) {
      value = values.shared_setting;
      result.effective_source = ::onc::kAugmentationSharedSetting;
    } else if (values.user_policy) {
      value = values.user_policy;
      result.effective_source = ::onc::kAugmentationUserPolicy;
    } else if (values.device_policy) {
      value = values.device_policy;
      result.effective_source = ::onc::kAugmentationDevicePolicy;
    } else {
      // Can be reached if the current field is recommended, but none of the
      // dictionaries contained a value for it.
    }

    if (value) {
      result.effective_value = value->Clone();
    }
    return result;
  }

  // MergeSettingsAndPolicies override.
  base::Value MergeValues(const std::string& key,
                          const ValueParams& values) override {
    std::string which;
    return MergeValuesImpl(key, values).effective_value;
  }
};

// Returns true if all not-null values in |values| are equal to |value|.
bool AllPresentValuesEqual(const MergeSettingsAndPolicies::ValueParams& values,
                           const base::Value& value) {
  if (values.user_policy && value != *values.user_policy) {
    return false;
  }
  if (values.device_policy && value != *values.device_policy) {
    return false;
  }
  if (values.user_setting && value != *values.user_setting) {
    return false;
  }
  if (values.shared_setting && value != *values.shared_setting) {
    return false;
  }
  if (values.active_setting && value != *values.active_setting) {
    return false;
  }
  return true;
}

// Call MergeDictionaries to merge policies and settings to an augmented
// dictionary which contains a dictionary for each value in the original
// dictionaries. See the description of MergeSettingsAndPoliciesToAugmented.
class MergeToAugmented : public MergeToEffective {
 public:
  MergeToAugmented() = default;

  MergeToAugmented(const MergeToAugmented&) = delete;
  MergeToAugmented& operator=(const MergeToAugmented&) = delete;

  base::Value::Dict MergeDictionaries(
      const chromeos::onc::OncValueSignature& signature,
      const base::Value::Dict* user_policy,
      const base::Value::Dict* device_policy,
      const base::Value::Dict* user_settings,
      const base::Value::Dict* shared_settings,
      const base::Value::Dict* active_settings) {
    signature_ = &signature;
    return MergeToEffective::MergeDictionaries(user_policy, device_policy,
                                               user_settings, shared_settings,
                                               active_settings);
  }

 protected:
  // MergeSettingsAndPolicies override.
  base::Value MergeValues(const std::string& key,
                          const ValueParams& values) override {
    const chromeos::onc::OncFieldSignature* field = nullptr;
    if (signature_) {
      field = chromeos::onc::GetFieldSignature(*signature_, key);
    }

    if (!field) {
      // This field is not part of the provided ONCSignature, thus it cannot be
      // controlled by policy. Return the plain active value instead of an
      // augmented dictionary.
      if (values.active_setting) {
        return values.active_setting->Clone();
      }
      return base::Value();
    }

    // This field is part of the provided ONCSignature, thus it can be
    // controlled by policy.
    MergeResult merge_result = MergeToEffective::MergeValuesImpl(key, values);

    if (IsReadOnlyField(*signature_, key)) {
      // Don't augment read-only fields (GUID and Type).
      if (!merge_result.effective_value.is_none()) {
        // DCHECK that all provided fields are identical.
        DCHECK(AllPresentValuesEqual(values, merge_result.effective_value))
            << "Values do not match: " << key
            << " Effective: " << merge_result.effective_value;
        // Return the un-augmented field.
        return std::move(merge_result.effective_value);
      }
      if (values.active_setting) {
        // Unmanaged networks have assigned (active) values.
        return values.active_setting->Clone();
      }
      LOG(ERROR) << "Field has no effective value: " << key;
      return base::Value();
    }

    base::Value::Dict augmented_value;

    if (values.active_setting) {
      augmented_value.Set(::onc::kAugmentationActiveSetting,
                          values.active_setting->Clone());
    }

    if (merge_result.effective_source) {
      augmented_value.Set(::onc::kAugmentationEffectiveSetting,
                          merge_result.effective_source);
    }

    // Prevent credentials from being forwarded in cleartext to UI. User/shared
    // credentials are not stored separately, so they cannot leak here. User and
    // Shared settings are already replaced with |kFakeCredential|.
    bool is_credential = chromeos::onc::FieldIsCredential(*signature_, key);
    if (is_credential) {
      // Set |kFakeCredential| to notify UI that credential is saved.
      if (values.user_policy) {
        augmented_value.Set(::onc::kAugmentationUserPolicy,
                            policy_util::kFakeCredential);
      }
      if (values.device_policy) {
        augmented_value.Set(::onc::kAugmentationDevicePolicy,
                            policy_util::kFakeCredential);
      }
      if (values.active_setting) {
        augmented_value.Set(::onc::kAugmentationActiveSetting,
                            policy_util::kFakeCredential);
      }
    } else {
      if (values.user_policy) {
        augmented_value.Set(::onc::kAugmentationUserPolicy,
                            values.user_policy->Clone());
      }
      if (values.device_policy) {
        augmented_value.Set(::onc::kAugmentationDevicePolicy,
                            values.device_policy->Clone());
      }
    }
    if (values.user_setting) {
      augmented_value.Set(::onc::kAugmentationUserSetting,
                          values.user_setting->Clone());
    }
    if (values.shared_setting) {
      augmented_value.Set(::onc::kAugmentationSharedSetting,
                          values.shared_setting->Clone());
    }

    if (HasUserPolicy() && values.user_editable) {
      augmented_value.Set(::onc::kAugmentationUserEditable, true);

      // Ensure that a property that is editable and has a user policy (which
      // indicates that the policy recommends a value) always has the
      // appropriate default user policy value provided.
      // TODO(b/245885527): Come up with a better handling for ONC defaults.
      SetIfNotSet(augmented_value, ::onc::kAugmentationUserPolicy,
                  GetDefaultValue(field));
      SetIfNotSet(augmented_value, ::onc::kAugmentationEffectiveSetting,
                  ::onc::kAugmentationUserPolicy);
    }
    if (HasDevicePolicy() && values.device_editable) {
      augmented_value.Set(::onc::kAugmentationDeviceEditable, true);

      // Ensure that a property that is editable and has a device policy (which
      // indicates that the policy recommends a value) always has the
      // appropriate default device policy value provided.
      // TODO(b/245885527): Come up with a better handling for ONC defaults.
      SetIfNotSet(augmented_value, ::onc::kAugmentationDevicePolicy,
                  GetDefaultValue(field));
      SetIfNotSet(augmented_value, ::onc::kAugmentationEffectiveSetting,
                  ::onc::kAugmentationDevicePolicy);
    }
    if (!augmented_value.empty()) {
      return base::Value(std::move(augmented_value));
    }

    return base::Value();
  }

  // MergeListOfDictionaries override.
  base::Value::Dict MergeNestedDictionaries(
      const std::string& key,
      const DictPointers& dicts) override {
    if (signature_) {
      const chromeos::onc::OncValueSignature* enclosing_signature = signature_;
      signature_ = nullptr;

      const chromeos::onc::OncFieldSignature* field =
          chromeos::onc::GetFieldSignature(*enclosing_signature, key);
      if (field) {
        signature_ = field->value_signature;
      }
      base::Value::Dict result =
          MergeToEffective::MergeNestedDictionaries(key, dicts);
      signature_ = enclosing_signature;
      return result;
    }
    return MergeToEffective::MergeNestedDictionaries(key, dicts);
  }

 private:
  raw_ptr<const chromeos::onc::OncValueSignature> signature_;
};

}  // namespace

base::Value::Dict MergeSettingsAndPoliciesToEffective(
    const base::Value::Dict* user_policy,
    const base::Value::Dict* device_policy,
    const base::Value::Dict* user_settings,
    const base::Value::Dict* shared_settings) {
  MergeToEffective merger;
  return merger.MergeDictionaries(user_policy, device_policy, user_settings,
                                  shared_settings, nullptr);
}

base::Value::Dict MergeSettingsAndPoliciesToAugmented(
    const chromeos::onc::OncValueSignature& signature,
    const base::Value::Dict* user_policy,
    const base::Value::Dict* device_policy,
    const base::Value::Dict* user_settings,
    const base::Value::Dict* shared_settings,
    const base::Value::Dict* active_settings) {
  MergeToAugmented merger;
  return merger.MergeDictionaries(signature, user_policy, device_policy,
                                  user_settings, shared_settings,
                                  active_settings);
}

}  // namespace ash::onc