chromium/chromeos/components/onc/onc_validator.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/components/onc/onc_validator.h"

#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <optional>
#include <string_view>
#include <utility>

#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chromeos/components/onc/onc_signature.h"
#include "components/crx_file/id_util.h"
#include "components/device_event_log/device_event_log.h"
#include "components/onc/onc_constants.h"
#include "onc_signature.h"

namespace chromeos::onc {

namespace {

// According to the IEEE 802.11 standard the SSID is a series of 0 to 32 octets.
const int kMaximumSSIDLengthInBytes = 32;

// Valid top-level configuration types
const std::vector<const char*>& GetValidToplevelConfigurationTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::toplevel_config::kUnencryptedConfiguration,
       ::onc::toplevel_config::kEncryptedConfiguration});
  return *valid_values;
}

// Valid network types
const std::vector<const char*>& GetValidNetworkTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::network_type::kEthernet, ::onc::network_type::kVPN,
       ::onc::network_type::kWiFi, ::onc::network_type::kCellular,
       ::onc::network_type::kTether});
  return *valid_values;
}

// Valid cellular IP configuration types
const std::vector<const char*>& GetValidIPConfigTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::network_config::kIPConfigTypeDHCP,
       ::onc::network_config::kIPConfigTypeStatic});
  return *valid_values;
}

// Valid check captive portal values
const std::vector<const char*>& GetValidCheckCaptivePortalValues() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::check_captive_portal::kTrue, ::onc::check_captive_portal::kFalse,
       ::onc::check_captive_portal::kHTTPOnly});
  return *valid_values;
}

// Valid cellular APN IP types
const std::vector<const char*>& GetValidAPNIpTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::cellular_apn::kIpTypeAutomatic, ::onc::cellular_apn::kIpTypeIpv4,
       ::onc::cellular_apn::kIpTypeIpv6, ::onc::cellular_apn::kIpTypeIpv4Ipv6});
  return *valid_values;
}

// Valid APN types
const std::vector<const char*>& GetValidApnTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::cellular_apn::kApnTypeDefault,
       ::onc::cellular_apn::kApnTypeAttach,
       ::onc::cellular_apn::kApnTypeTether});
  return *valid_values;
}

// Valid ethernet authentications
const std::vector<const char*>& GetValidEthernetAuthentications() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::ethernet::kAuthenticationNone, ::onc::ethernet::k8021X});
  return *valid_values;
}

// Valid network IP config types
const std::vector<const char*>& GetValidNetworkIPConfigTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::ipconfig::kIPv4, ::onc::ipconfig::kIPv6});
  return *valid_values;
}

// Valid Wi-Fi securities
const std::vector<const char*>& GetValidWiFiSecurities() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::wifi::kSecurityNone, ::onc::wifi::kWEP_PSK,
       ::onc::wifi::kWEP_8021X, ::onc::wifi::kWPA_PSK, ::onc::wifi::kWPA_EAP});
  return *valid_values;
}

// Valid IPSec authentications
const std::vector<const char*>& GetValidIPsecAuthentications() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::ipsec::kPSK, ::onc::ipsec::kCert, ::onc::ipsec::kEAP});
  return *valid_values;
}

// Valid OpenVPN auth retry values
const std::vector<const char*>& GetValidVPNAuthRetryValues() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::openvpn::kNone, ::onc::openvpn::kInteract,
       ::onc::openvpn::kNoInteract});
  return *valid_values;
}

// Valid OpenVPN cert TLS values
const std::vector<const char*>& GetValidVPNCertTlsValues() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::openvpn::kNone, ::onc::openvpn::kServer});
  return *valid_values;
}

// Valid OpenVPN compression algorithm values
const std::vector<const char*>& GetValidVPNCompressionAlgorithmValues() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::openvpn_compression_algorithm::kFramingOnly,
       ::onc::openvpn_compression_algorithm::kLz4,
       ::onc::openvpn_compression_algorithm::kLz4V2,
       ::onc::openvpn_compression_algorithm::kLzo,
       ::onc::openvpn_compression_algorithm::kNone});
  return *valid_values;
}

// Valid OpenVPN user auth types
const std::vector<const char*>& GetValidVPNUserAuthTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::openvpn_user_auth_type::kNone,
       ::onc::openvpn_user_auth_type::kOTP,
       ::onc::openvpn_user_auth_type::kPassword,
       ::onc::openvpn_user_auth_type::kPasswordAndOTP});
  return *valid_values;
}

// Valid X.509 types
const std::vector<const char*>& GetValidX509Types() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::verify_x509::types::kName, ::onc::verify_x509::types::kNamePrefix,
       ::onc::verify_x509::types::kSubject});
  return *valid_values;
}

// Valid allow text messages types
const std::vector<const char*>& GetValidAllowTextMessagesTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::cellular::kTextMessagesAllow,
       ::onc::cellular::kTextMessagesSuppress,
       ::onc::cellular::kTextMessagesUnset});
  return *valid_values;
}

// Valid proxy settings types
const std::vector<const char*>& GetValidProxySettingsTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::proxy::kDirect, ::onc::proxy::kManual, ::onc::proxy::kPAC,
       ::onc::proxy::kWPAD});
  return *valid_values;
}

// Valid EAP inner values
const std::vector<const char*>& GetValidEAPInnerValues() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::eap::kAutomatic, ::onc::eap::kGTC, ::onc::eap::kMD5,
       ::onc::eap::kMSCHAPv2, ::onc::eap::kPAP});
  return *valid_values;
}

// Valid EAP outer values
const std::vector<const char*>& GetValidEAPOuterValues() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::eap::kPEAP, ::onc::eap::kEAP_TLS, ::onc::eap::kEAP_TTLS,
       ::onc::eap::kLEAP, ::onc::eap::kEAP_SIM, ::onc::eap::kEAP_FAST,
       ::onc::eap::kEAP_AKA});
  return *valid_values;
}

// Valid EAP Subject Alternative Name match types
const std::vector<const char*>& GetValidEAPSubjectAlternativeNameMatchTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::eap_subject_alternative_name_match::kEMAIL,
       ::onc::eap_subject_alternative_name_match::kDNS,
       ::onc::eap_subject_alternative_name_match::kURI});
  return *valid_values;
}

// Valid certificate types
const std::vector<const char*>& GetValidCertificateTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::certificate::kClient, ::onc::certificate::kServer,
       ::onc::certificate::kAuthority});
  return *valid_values;
}

// Valid scope types
const std::vector<const char*>& GetValidScopeTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::scope::kDefault, ::onc::scope::kExtension});
  return *valid_values;
}

// All valid EAP types
const std::vector<const char*>& GetAllValidVPNTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values(
      {::onc::vpn::kIPsec, ::onc::vpn::kTypeL2TP_IPsec, ::onc::vpn::kOpenVPN,
       ::onc::vpn::kWireGuard, ::onc::vpn::kThirdPartyVpn, ::onc::vpn::kArcVpn

      });
  return *valid_values;
}

// Valid managed EAP types
const std::vector<const char*>& GetValidManagedVPNTypes() {
  static const base::NoDestructor<std::vector<const char*>> valid_values({
      ::onc::vpn::kIPsec,
      ::onc::vpn::kTypeL2TP_IPsec,
      ::onc::vpn::kOpenVPN,
      ::onc::vpn::kWireGuard,
  });
  return *valid_values;
}

void AddKeyToList(const char* key, base::Value::List* list) {
  base::Value key_value(key);
  if (!base::Contains(*list, key_value)) {
    list->Append(std::move(key_value));
  }
}

std::string GetStringFromDict(const base::Value::Dict& dict, const char* key) {
  const std::string* value = dict.FindString(key);
  return value ? *value : std::string();
}

base::flat_set<std::string> GetStringsFromDicts(const base::Value::List& dicts,
                                                const char* key) {
  base::flat_set<std::string> values;
  for (const base::Value& dict : dicts) {
    if (!dict.is_dict()) {
      continue;
    }

    const std::string* value = dict.GetDict().FindString(key);
    if (!value) {
      continue;
    }

    values.emplace(*value);
  }
  return values;
}

bool FieldIsRecommended(const base::Value::Dict& object,
                        const std::string& field_name) {
  const base::Value::List* recommended = object.FindList(::onc::kRecommended);
  return recommended && base::Contains(*recommended, base::Value(field_name));
}

bool FieldIsSetToValueOrRecommended(const base::Value::Dict& object,
                                    const std::string& field_name,
                                    const base::Value& expected_value) {
  const base::Value* actual_value = object.Find(field_name);
  if (actual_value && expected_value == *actual_value)
    return true;

  return FieldIsRecommended(object, field_name);
}

// Determines whether the values associated with a specific key within a list of
// dictionaries are all unique.
bool HasUniqueValuesForKeyInDicts(const base::Value::List& dicts,
                                  const std::string& key) {
  base::flat_set<base::Value> seen_values;
  for (const base::Value& dict : dicts) {
    if (!dict.is_dict()) {
      return false;
    }

    const base::Value* value = dict.GetDict().Find(key);
    if (!value || seen_values.count(*value) > 0) {
      return false;
    }

    seen_values.insert(value->Clone());
  }

  return true;
}

}  // namespace

Validator::Validator(bool error_on_unknown_field,
                     bool error_on_wrong_recommended,
                     bool error_on_missing_field,
                     bool managed_onc,
                     bool log_warnings)
    : error_on_unknown_field_(error_on_unknown_field),
      error_on_wrong_recommended_(error_on_wrong_recommended),
      error_on_missing_field_(error_on_missing_field),
      managed_onc_(managed_onc),
      log_warnings_(log_warnings) {}

Validator::~Validator() = default;

std::optional<base::Value::Dict> Validator::ValidateAndRepairObject(
    const OncValueSignature* object_signature,
    const base::Value::Dict& onc_object,
    Result* result) {
  CHECK(object_signature);
  *result = VALID;
  bool error = false;
  base::Value::Dict result_value =
      MapObject(*object_signature, onc_object, &error);
  if (error) {
    *result = INVALID;
    return std::nullopt;
  }
  if (!validation_issues_.empty()) {
    *result = VALID_WITH_WARNINGS;
  }
  return result_value;
}

base::Value Validator::MapValue(const OncValueSignature& signature,
                                const base::Value& onc_value,
                                bool* error) {
  if (onc_value.type() != signature.onc_type) {
    *error = true;
    std::ostringstream msg;
    msg << "Found value of type '" << base::Value::GetTypeName(onc_value.type())
        << "', but type '" << base::Value::GetTypeName(signature.onc_type)
        << "' is required.";
    AddValidationIssue(true /* is_error */, msg.str());
    return {};
  }

  base::Value repaired = Mapper::MapValue(signature, onc_value, error);
  CHECK(repaired.is_none() || repaired.type() == signature.onc_type);
  return repaired;
}

base::Value::Dict Validator::MapObject(const OncValueSignature& signature,
                                       const base::Value::Dict& onc_object,
                                       bool* error) {
  base::Value::Dict repaired;
  bool valid = ValidateObjectDefault(signature, onc_object, &repaired);

  if (valid) {
    if (&signature == &kToplevelConfigurationSignature) {
      valid = ValidateToplevelConfiguration(&repaired);
    } else if (&signature == &kNetworkConfigurationSignature) {
      valid = ValidateNetworkConfiguration(&repaired);
    } else if (&signature == &kCellularSignature) {
      valid = ValidateCellular(&repaired);
    } else if (&signature == &kCellularApnSignature) {
      valid = ValidateAPN(&repaired);
    } else if (&signature == &kEthernetSignature) {
      valid = ValidateEthernet(&repaired);
    } else if (&signature == &kIPConfigSignature ||
               &signature == &kSavedIPConfigSignature) {
      valid = ValidateIPConfig(&repaired);
    } else if (&signature == &kWiFiSignature) {
      valid = ValidateWiFi(&repaired);
    } else if (&signature == &kVPNSignature) {
      valid = ValidateVPN(&repaired);
    } else if (&signature == &kIPsecSignature) {
      valid = ValidateIPsec(&repaired);
    } else if (&signature == &kOpenVPNSignature) {
      valid = ValidateOpenVPN(&repaired);
    } else if (&signature == &kWireGuardSignature) {
      valid = ValidateWireGuard(&repaired);
    } else if (&signature == &kThirdPartyVPNSignature) {
      valid = ValidateThirdPartyVPN(&repaired);
    } else if (&signature == &kARCVPNSignature) {
      valid = ValidateARCVPN(&repaired);
    } else if (&signature == &kVerifyX509Signature) {
      valid = ValidateVerifyX509(&repaired);
    } else if (&signature == &kCertificatePatternSignature) {
      valid = ValidateCertificatePattern(&repaired);
    } else if (&signature == &kGlobalNetworkConfigurationSignature) {
      valid = ValidateGlobalNetworkConfiguration(&repaired);
    } else if (&signature == &kProxySettingsSignature) {
      valid = ValidateProxySettings(&repaired);
    } else if (&signature == &kProxyLocationSignature) {
      valid = ValidateProxyLocation(&repaired);
    } else if (&signature == &kEAPSignature) {
      valid = ValidateEAP(&repaired);
    } else if (&signature == &kEAPSubjectAlternativeNameMatchSignature) {
      valid = ValidateSubjectAlternativeNameMatch(&repaired);
    } else if (&signature == &kCertificateSignature) {
      valid = ValidateCertificate(&repaired);
    } else if (&signature == &kScopeSignature) {
      valid = ValidateScope(&repaired);
    } else if (&signature == &kTetherWithStateSignature) {
      valid = ValidateTether(&repaired);
    }

    // StaticIPConfig is not validated here, because its correctness depends
    // on NetworkConfiguration's 'IPAddressConfigType', 'NameServersConfigType'
    // and 'Recommended' fields. It's validated in
    // ValidateNetworkConfiguration() instead.
  }

  if (valid)
    return repaired;

  DCHECK(!validation_issues_.empty());
  *error = true;
  return base::Value::Dict();
}

base::Value Validator::MapField(const std::string& field_name,
                                const OncValueSignature& object_signature,
                                const base::Value& onc_value,
                                bool* found_unknown_field,
                                bool* error) {
  path_.push_back(field_name);
  bool current_field_unknown = false;
  base::Value result = Mapper::MapField(field_name, object_signature, onc_value,
                                        &current_field_unknown, error);

  DCHECK_EQ(field_name, path_.back());
  path_.pop_back();

  if (current_field_unknown) {
    *found_unknown_field = true;
    std::ostringstream msg;
    msg << "Field name '" << field_name << "' is unknown.";
    AddValidationIssue(error_on_unknown_field_, msg.str());
  }

  return result;
}

base::Value::List Validator::MapArray(const OncValueSignature& array_signature,
                                      const base::Value::List& onc_array,
                                      bool* nested_error) {
  bool nested_error_in_current_array = false;
  base::Value::List result = Mapper::MapArray(array_signature, onc_array,
                                              &nested_error_in_current_array);

  if (&array_signature == &kNetworkConfigurationListSignature) {
    ValidateEthernetConfigs(&result);
  }

  // Drop individual networks and certificates instead of rejecting all of
  // the configuration.
  if (nested_error_in_current_array &&
      &array_signature != &kNetworkConfigurationListSignature &&
      &array_signature != &kCertificateListSignature &&
      &array_signature != &kAdminApnListSignature) {
    *nested_error = nested_error_in_current_array;
  }
  return result;
}

base::Value Validator::MapEntry(int index,
                                const OncValueSignature& signature,
                                const base::Value& onc_value,
                                bool* error) {
  std::string index_as_string = base::NumberToString(index);
  path_.push_back(index_as_string);
  base::Value result = Mapper::MapEntry(index, signature, onc_value, error);
  DCHECK_EQ(index_as_string, path_.back());
  path_.pop_back();
  if (result.is_none() && (&signature == &kNetworkConfigurationSignature ||
                           &signature == &kCertificateSignature)) {
    std::ostringstream msg;
    msg << "Entry at index '" << index_as_string
        << "' has been removed because it contained errors.";
    AddValidationIssue(false /* is_error */, msg.str());
  }
  return result;
}

bool Validator::ValidateObjectDefault(const OncValueSignature& signature,
                                      const base::Value::Dict& onc_object,
                                      base::Value::Dict* result) {
  bool found_unknown_field = false;
  bool nested_error_occurred = false;
  MapFields(signature, onc_object, &found_unknown_field, &nested_error_occurred,
            result);

  if (found_unknown_field && error_on_unknown_field_) {
    DVLOG(1) << "Unknown field names are errors: Aborting.";
    return false;
  }

  if (nested_error_occurred) {
    return false;
  }

  return ValidateRecommendedField(signature, result);
}

bool Validator::ValidateRecommendedField(
    const OncValueSignature& object_signature,
    base::Value::Dict* result) {
  CHECK(result);

  std::optional<base::Value> recommended_value =
      result->Extract(::onc::kRecommended);
  // This remove passes ownership to |recommended_value|.
  if (!recommended_value) {
    return true;
  }

  // The types of field values are already verified.
  DCHECK(recommended_value->is_list());

  if (!managed_onc_) {
    std::ostringstream msg;
    msg << "Found the field '" << ::onc::kRecommended
        << "' in an unmanaged ONC";
    AddValidationIssue(false /* is_error */, msg.str());
    return true;
  }

  base::Value::List repaired_recommended;
  for (const auto& entry : recommended_value->GetList()) {
    const std::string* field_name = entry.GetIfString();
    if (!field_name) {
      NOTREACHED_IN_MIGRATION();  // The types of field values are already
                                  // verified.
      continue;
    }

    const OncFieldSignature* field_signature =
        GetFieldSignature(object_signature, *field_name);

    bool found_error = false;
    std::string error_cause;
    if (!field_signature) {
      found_error = true;
      error_cause = "unknown";
    } else if (field_signature->value_signature->onc_type ==
               base::Value::Type::DICT) {
      found_error = true;
      error_cause = "dictionary-typed";
    }

    if (found_error) {
      path_.push_back(::onc::kRecommended);
      std::ostringstream msg;
      msg << "The " << error_cause << " field '" << *field_name
          << "' cannot be recommended.";
      AddValidationIssue(error_on_wrong_recommended_, msg.str());
      path_.pop_back();
      if (error_on_wrong_recommended_)
        return false;
      continue;
    }

    repaired_recommended.Append(*field_name);
  }

  result->Set(::onc::kRecommended, std::move(repaired_recommended));
  return true;
}

bool Validator::ValidateClientCertFields(bool allow_cert_type_none,
                                         base::Value::Dict* result) {
  std::vector<const char*> valid_cert_types = {
      ::onc::client_cert::kRef, ::onc::client_cert::kPattern,
      ::onc::client_cert::kProvisioningProfileId,
      ::onc::client_cert::kPKCS11Id};
  if (allow_cert_type_none)
    valid_cert_types.push_back(::onc::client_cert::kClientCertTypeNone);

  std::string cert_type =
      GetStringFromDict(*result, ::onc::client_cert::kClientCertType);

  // TODO(crbug.com/40117885): Remove the client certificate type empty
  // check. Ignored fields should be removed by normalizer before validating.
  if (cert_type.empty())
    return true;

  if (!IsValidValue(cert_type, valid_cert_types))
    return false;

  bool all_required_exist = true;

  if (cert_type == ::onc::client_cert::kProvisioningProfileId)
    all_required_exist &= RequireField(
        *result, ::onc::client_cert::kClientCertProvisioningProfileId);
  else if (cert_type == ::onc::client_cert::kPattern)
    all_required_exist &=
        RequireField(*result, ::onc::client_cert::kClientCertPattern);
  else if (cert_type == ::onc::client_cert::kRef)
    all_required_exist &=
        RequireField(*result, ::onc::client_cert::kClientCertRef);
  else if (cert_type == ::onc::client_cert::kPKCS11Id)
    all_required_exist &=
        RequireField(*result, ::onc::client_cert::kClientCertPKCS11Id);

  return !error_on_missing_field_ || all_required_exist;
}

namespace {

std::string JoinStringRange(const std::vector<const char*>& strings,
                            const std::string& separator) {
  std::vector<std::string_view> string_vector(strings.begin(), strings.end());
  return base::JoinString(string_vector, separator);
}

}  // namespace

bool Validator::IsInDevicePolicy(base::Value::Dict* result,
                                 std::string_view field_name) {
  if (result->contains(field_name)) {
    if (onc_source_ != ::onc::ONC_SOURCE_DEVICE_POLICY) {
      std::ostringstream msg;
      msg << "Field '" << field_name << "' is only allowed in a device policy.";
      AddValidationIssue(true /* is_error */, msg.str());
      return false;
    }
  }
  return true;
}

bool Validator::IsValidValue(const std::string& field_value,
                             const std::vector<const char*>& valid_values) {
  for (const char* it : valid_values) {
    if (field_value == it)
      return true;
  }

  std::ostringstream msg;
  msg << "Found value '" << field_value << "', but expected one of the values ["
      << JoinStringRange(valid_values, ", ") << "]";
  AddValidationIssue(true /* is_error */, msg.str());
  return false;
}

bool Validator::FieldExistsAndHasNoValidValue(
    const base::Value::Dict& object,
    const std::string& field_name,
    const std::vector<const char*>& valid_values) {
  const std::string* actual_value = object.FindString(field_name);
  if (!actual_value)
    return false;

  path_.push_back(field_name);
  const bool valid = IsValidValue(*actual_value, valid_values);
  path_.pop_back();
  return !valid;
}

bool Validator::FieldExistsAndIsNotInRange(const base::Value::Dict& object,
                                           const std::string& field_name,
                                           int lower_bound,
                                           int upper_bound) {
  std::optional<int> actual_value = object.FindInt(field_name);
  if (!actual_value || (lower_bound <= actual_value.value() &&
                        actual_value.value() <= upper_bound)) {
    return false;
  }

  path_.push_back(field_name);
  std::ostringstream msg;
  msg << "Found value '" << actual_value.value()
      << "', but expected a value in the range [" << lower_bound << ", "
      << upper_bound << "] (boundaries inclusive)";
  AddValidationIssue(true /* is_error */, msg.str());
  path_.pop_back();
  return true;
}

bool Validator::FieldExistsAndIsEmpty(const base::Value::Dict& dict,
                                      const std::string& field_name) {
  if (!dict.contains(field_name)) {
    return false;
  }

  const std::string* maybe_str = dict.FindString(field_name);
  const base::Value::List* maybe_list = dict.FindList(field_name);
  if (maybe_str) {
    if (!(*maybe_str).empty()) {
      return false;
    }
  } else if (maybe_list) {
    if (!(*maybe_list).empty()) {
      return false;
    }
  } else {
    NOTREACHED_IN_MIGRATION();
    return false;
  }

  path_.push_back(field_name);
  std::ostringstream msg;
  msg << "Found an empty string, but expected a non-empty string.";
  AddValidationIssue(/*is_error=*/true, /*debug_info=*/msg.str());
  path_.pop_back();
  return true;
}

bool Validator::FieldShouldExistOrBeRecommended(const base::Value::Dict& object,
                                                const std::string& field_name) {
  if (object.contains(field_name) || FieldIsRecommended(object, field_name)) {
    return true;
  }

  std::ostringstream msg;
  msg << "Field " << field_name << " is not found, but expected either to be "
      << "set or to be recommended.";
  AddValidationIssue(error_on_missing_field_, msg.str());
  return !error_on_missing_field_;
}

bool Validator::OnlyOneFieldSet(const base::Value::Dict& object,
                                const std::string& field_name1,
                                const std::string& field_name2) {
  if (object.contains(field_name1) && object.contains(field_name2)) {
    std::ostringstream msg;
    msg << "At most one of '" << field_name1 << "' and '" << field_name2
        << "' can be set.";
    AddValidationIssue(true /* is_error */, msg.str());
    return false;
  }
  return true;
}

bool Validator::ListFieldContainsValidValues(
    const base::Value::Dict& object,
    const std::string& field_name,
    const std::vector<const char*>& valid_values) {
  const base::Value::List* list = object.FindList(field_name);
  if (!list)
    return true;
  path_.push_back(field_name);
  for (const auto& entry : *list) {
    const std::string* value = entry.GetIfString();
    if (!value) {
      NOTREACHED_IN_MIGRATION();  // The types of field values are already
                                  // verified.
      continue;
    }
    if (!IsValidValue(*value, valid_values)) {
      path_.pop_back();
      return false;
    }
  }
  path_.pop_back();
  return true;
}

bool Validator::ValidateSSIDAndHexSSID(base::Value::Dict* object) {
  const std::string kInvalidLength = "Invalid length";

  // Check SSID validity.
  std::string* ssid_string = object->FindString(::onc::wifi::kSSID);
  if (ssid_string && (ssid_string->size() <= 0 ||
                      ssid_string->size() > kMaximumSSIDLengthInBytes)) {
    path_.push_back(::onc::wifi::kSSID);
    std::ostringstream msg;
    msg << kInvalidLength;
    // If the HexSSID field is present, ignore errors in SSID because these
    // might be caused by the usage of a non-UTF-8 encoding when the SSID
    // field was automatically added (see FillInHexSSIDField).
    if (!object->contains(::onc::wifi::kHexSSID)) {
      AddValidationIssue(true /* is_error */, msg.str());
      path_.pop_back();
      return false;
    }
    AddValidationIssue(false /* is_error */, msg.str());
    path_.pop_back();
  }

  // Check HexSSID validity.
  std::string* hex_ssid_string = object->FindString(::onc::wifi::kHexSSID);
  if (!hex_ssid_string)
    return true;

  std::string decoded_ssid;
  if (!base::HexStringToString(*hex_ssid_string, &decoded_ssid)) {
    path_.push_back(::onc::wifi::kHexSSID);
    std::ostringstream msg;
    msg << "Not a valid hex representation: '" << *hex_ssid_string << "'";
    AddValidationIssue(true /* is_error */, msg.str());
    path_.pop_back();
    return false;
  }
  if (decoded_ssid.size() <= 0 ||
      decoded_ssid.size() > kMaximumSSIDLengthInBytes) {
    path_.push_back(::onc::wifi::kHexSSID);
    std::ostringstream msg;
    msg << kInvalidLength;
    AddValidationIssue(true /* is_error */, msg.str());
    path_.pop_back();
    return false;
  }

  // If both SSID and HexSSID are set, check whether they are consistent, i.e.
  // HexSSID contains the UTF-8 encoding of SSID. If not, remove the SSID
  // field.
  if (ssid_string && ssid_string->length() > 0) {
    if (*ssid_string != decoded_ssid) {
      path_.push_back(::onc::wifi::kSSID);
      std::ostringstream msg;
      msg << "Fields '" << ::onc::wifi::kSSID << "' and '"
          << ::onc::wifi::kHexSSID << "' contain inconsistent values.";
      AddValidationIssue(false /* is_error */, msg.str());
      path_.pop_back();
      object->Remove(::onc::wifi::kSSID);
    }
  }
  return true;
}

bool Validator::RequireField(const base::Value::Dict& dict,
                             const std::string& field_name) {
  if (dict.contains(field_name)) {
    return true;
  }

  std::ostringstream msg;
  msg << "The required field '" << field_name << "' is missing.";
  AddValidationIssue(error_on_missing_field_, msg.str());
  return false;
}

bool Validator::CheckAdminAssignedAPNIdsAreNonEmptyAndAddToSet(
    const base::Value::Dict& dict,
    const std::string& key_list_of_ids) {
  CHECK(key_list_of_ids == ::onc::cellular::kAdminAssignedAPNIds ||
        key_list_of_ids ==
            ::onc::global_network_config::kPSIMAdminAssignedAPNIds);

  const base::Value::List* id_list = dict.FindList(key_list_of_ids);
  if (!id_list) {
    return true;
  }

  for (const base::Value& id_value : *id_list) {
    const std::string id = id_value.GetString();
    if (id.empty()) {
      std::ostringstream msg;
      msg << key_list_of_ids << " must only include non-empty IDs";
      AddValidationIssue(true /* is_error */, msg.str());
      return false;
    }
    admin_assigned_apn_ids_.emplace(id);
  }
  return true;
}

bool Validator::CheckGuidIsUniqueAndAddToSet(const base::Value::Dict& dict,
                                             const std::string& key_guid,
                                             std::set<std::string>* guids) {
  const std::string* guid = dict.FindString(key_guid);
  if (!guid) {
    return true;
  }

  if (guids->count(*guid) != 0) {
    path_.push_back(key_guid);
    std::ostringstream msg;
    msg << "Found a duplicate GUID '" << *guid << "'.";
    AddValidationIssue(true /* is_error */, msg.str());
    path_.pop_back();
    return false;
  }
  guids->insert(*guid);
  return true;
}

bool Validator::IsGlobalNetworkConfigInUserImport(
    const base::Value::Dict& onc_object) {
  if (onc_source_ == ::onc::ONC_SOURCE_USER_IMPORT &&
      onc_object.contains(
          ::onc::toplevel_config::kGlobalNetworkConfiguration)) {
    std::ostringstream msg;
    msg << "Field '" << ::onc::toplevel_config::kGlobalNetworkConfiguration
        << "' is prohibited in ONC user imports";
    AddValidationIssue(true /* is_error */, msg.str());
    return true;
  }
  return false;
}

bool Validator::ValidateToplevelConfiguration(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::toplevel_config::kType,
                                    GetValidToplevelConfigurationTypes())) {
    return false;
  }

  base::Value::List* admin_apn_list =
      result->FindList(::onc::toplevel_config::kAdminAPNList);

  // Enforces unique string identifiers for APNs within the 'AdminAPNList'. Note
  // that duplicate identifiers may still exist in other APN arrays due to
  // sources (like the modem or modb) that don't provide unique Ids.
  if (admin_apn_list) {
    if (!HasUniqueValuesForKeyInDicts(*admin_apn_list,
                                      ::onc::cellular_apn::kId)) {
      AddValidationIssue(/*is_error=*/true,
                         "APNs in the AdminAPNList do not have unique IDs");
      return false;
    }

    base::flat_set<std::string> ids_of_toplevel_apns =
        GetStringsFromDicts(*admin_apn_list, ::onc::cellular_apn::kId);
    if (!std::includes(ids_of_toplevel_apns.begin(), ids_of_toplevel_apns.end(),
                       admin_assigned_apn_ids_.begin(),
                       admin_assigned_apn_ids_.end())) {
      AddValidationIssue(/*is_error=*/true,
                         "Some cellular network configurations have admin APN "
                         "IDs that are not sourced from the admin");
      return false;
    }
  }

  if (IsGlobalNetworkConfigInUserImport(*result)) {
    return false;
  }

  return true;
}

bool Validator::ValidateNetworkConfiguration(base::Value::Dict* result) {
  const std::string* onc_type =
      result->FindString(::onc::network_config::kType);
  if (onc_type && *onc_type == ::onc::network_type::kWimaxDeprecated) {
    AddValidationIssue(/*is_error=*/false, "WiMax is deprecated");
    return true;
  }

  if (FieldExistsAndHasNoValidValue(*result, ::onc::network_config::kType,
                                    GetValidNetworkTypes()) ||
      FieldExistsAndHasNoValidValue(*result,
                                    ::onc::network_config::kIPAddressConfigType,
                                    GetValidIPConfigTypes()) ||
      FieldExistsAndHasNoValidValue(
          *result, ::onc::network_config::kNameServersConfigType,
          GetValidIPConfigTypes()) ||
      FieldExistsAndHasNoValidValue(*result,
                                    ::onc::network_config::kCheckCaptivePortal,
                                    GetValidCheckCaptivePortalValues()) ||
      FieldExistsAndIsEmpty(*result, ::onc::network_config::kGUID)) {
    return false;
  }

  if (!CheckGuidIsUniqueAndAddToSet(*result, ::onc::network_config::kGUID,
                                    &network_guids_))
    return false;

  bool all_required_exist = RequireField(*result, ::onc::network_config::kGUID);

  bool remove = result->FindBool(::onc::kRemove).value_or(false);
  if (!remove) {
    all_required_exist &= RequireField(*result, ::onc::network_config::kName) &&
                          RequireField(*result, ::onc::network_config::kType);

    if (!NetworkHasCorrectStaticIPConfig(result))
      return false;

    std::string type = GetStringFromDict(*result, ::onc::network_config::kType);

    // Prohibit anything but WiFi, Ethernet, VPN and Cellular for device-level
    // policy (which corresponds to shared networks). See also
    // http://crosbug.com/28741.
    if (onc_source_ == ::onc::ONC_SOURCE_DEVICE_POLICY && !type.empty() &&
        type != ::onc::network_type::kVPN &&
        type != ::onc::network_type::kWiFi &&
        type != ::onc::network_type::kEthernet &&
        type != ::onc::network_type::kCellular) {
      std::ostringstream msg;
      msg << "Networks of type '" << type
          << "' are prohibited in ONC device policies.";
      AddValidationIssue(true /* is_error */, msg.str());
      return false;
    }
    if (type == ::onc::network_type::kWiFi) {
      all_required_exist &= RequireField(*result, ::onc::network_config::kWiFi);
    } else if (type == ::onc::network_type::kEthernet) {
      all_required_exist &=
          RequireField(*result, ::onc::network_config::kEthernet);
    } else if (type == ::onc::network_type::kCellular) {
      all_required_exist &=
          RequireField(*result, ::onc::network_config::kCellular);
    } else if (type == ::onc::network_type::kWimaxDeprecated) {
      all_required_exist &=
          RequireField(*result, ::onc::network_config::kWimaxDeprecated);
    } else if (type == ::onc::network_type::kVPN) {
      all_required_exist &= RequireField(*result, ::onc::network_config::kVPN);
    } else if (type == ::onc::network_type::kTether) {
      all_required_exist &=
          RequireField(*result, ::onc::network_config::kTether);
    } else if (type == ::onc::network_type::kCellular) {
      all_required_exist &=
          RequireField(*result, ::onc::network_config::kCellular);
    }
  }

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateCellular(base::Value::Dict* result) {
  if (result->contains(::onc::cellular::kSMDPAddress) &&
      result->contains(::onc::cellular::kSMDSAddress)) {
    AddValidationIssue(
        /*is_error=*/true,
        R"(The "SMDPAddress" and "SMDSAddress" fields are mutually exclusive.)");
    return false;
  }

  if (!CheckAdminAssignedAPNIdsAreNonEmptyAndAddToSet(
          *result, ::onc::cellular::kAdminAssignedAPNIds)) {
    return false;
  }

  return true;
}

bool Validator::ValidateAPN(base::Value::Dict* result) {
  if (!RequireField(*result, ::onc::cellular_apn::kAccessPointName) ||
      FieldExistsAndIsEmpty(*result, ::onc::cellular_apn::kAccessPointName)) {
    return false;
  }

  if (FieldExistsAndHasNoValidValue(*result, ::onc::cellular_apn::kIpType,
                                    GetValidAPNIpTypes())) {
    return false;
  }

  if (FieldExistsAndIsEmpty(*result, ::onc::cellular_apn::kApnTypes) ||
      !ListFieldContainsValidValues(*result, ::onc::cellular_apn::kApnTypes,
                                    GetValidApnTypes())) {
    return false;
  }

  // TODO(b/333100319): Validate that all APNs with ::onc::cellular_apn::kSource
  // that are ::onc::cellular_apn::kAdmin or ::onc::cellular_apn::kUi have a
  // non-empty string ::onc::cellular_apn::kId. This should be done after
  // kApnPolicies flag is moved to chromeos as it is in ash currently.

  return true;
}

bool Validator::ValidateEthernet(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::ethernet::kAuthentication,
                                    GetValidEthernetAuthentications())) {
    return false;
  }

  bool all_required_exist = true;
  std::string auth =
      GetStringFromDict(*result, ::onc::ethernet::kAuthentication);
  if (auth == ::onc::ethernet::k8021X)
    all_required_exist &= RequireField(*result, ::onc::ethernet::kEAP);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateIPConfig(base::Value::Dict* result,
                                 bool require_fields) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::ipconfig::kType,
                                    GetValidNetworkIPConfigTypes())) {
    return false;
  }

  std::string type = GetStringFromDict(*result, ::onc::ipconfig::kType);
  int lower_bound = 1;
  // In case of missing type, choose higher upper_bound.
  int upper_bound = (type == ::onc::ipconfig::kIPv4) ? 32 : 128;
  if (FieldExistsAndIsNotInRange(*result, ::onc::ipconfig::kRoutingPrefix,
                                 lower_bound, upper_bound)) {
    return false;
  }

  if (FieldExistsAndIsNotInRange(*result, ::onc::ipconfig::kMTU, 0,
                                 std::numeric_limits<int>::max())) {
    return false;
  }

  bool all_required_exist = true;
  if (require_fields) {
    all_required_exist &= RequireField(*result, ::onc::ipconfig::kIPAddress);
    all_required_exist &=
        RequireField(*result, ::onc::ipconfig::kRoutingPrefix);
    all_required_exist &= RequireField(*result, ::onc::ipconfig::kGateway);
  } else {
    all_required_exist &=
        FieldShouldExistOrBeRecommended(*result, ::onc::ipconfig::kIPAddress);
    all_required_exist &= FieldShouldExistOrBeRecommended(
        *result, ::onc::ipconfig::kRoutingPrefix);
    all_required_exist &=
        FieldShouldExistOrBeRecommended(*result, ::onc::ipconfig::kGateway);
  }

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::NetworkHasCorrectStaticIPConfig(base::Value::Dict* network) {
  bool must_have_ip_config = FieldIsSetToValueOrRecommended(
      *network, ::onc::network_config::kIPAddressConfigType,
      base::Value(::onc::network_config::kIPConfigTypeStatic));
  bool must_have_nameservers = FieldIsSetToValueOrRecommended(
      *network, ::onc::network_config::kNameServersConfigType,
      base::Value(::onc::network_config::kIPConfigTypeStatic));

  if (!must_have_ip_config && !must_have_nameservers)
    return true;

  if (!RequireField(*network, ::onc::network_config::kStaticIPConfig))
    return false;

  base::Value::Dict* static_ip_config =
      network->FindDict(::onc::network_config::kStaticIPConfig);
  bool valid = true;
  // StaticIPConfig should have all fields required by the corresponding
  // IPAddressConfigType and NameServersConfigType values.
  if (must_have_ip_config)
    valid &= ValidateIPConfig(static_ip_config, false /* require_fields */);
  if (must_have_nameservers)
    valid &= FieldShouldExistOrBeRecommended(*static_ip_config,
                                             ::onc::ipconfig::kNameServers);
  return valid;
}

bool Validator::ValidateWiFi(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::wifi::kSecurity,
                                    GetValidWiFiSecurities())) {
    return false;
  }

  if (!ValidateSSIDAndHexSSID(result))
    return false;

  bool all_required_exist = RequireField(*result, ::onc::wifi::kSecurity);

  // One of {kSSID, kHexSSID} must be present.
  if (!result->contains(::onc::wifi::kSSID)) {
    all_required_exist &= RequireField(*result, ::onc::wifi::kHexSSID);
  }
  if (!result->contains(::onc::wifi::kHexSSID)) {
    all_required_exist &= RequireField(*result, ::onc::wifi::kSSID);
  }

  std::string security = GetStringFromDict(*result, ::onc::wifi::kSecurity);
  if (security == ::onc::wifi::kWEP_8021X || security == ::onc::wifi::kWPA_EAP)
    all_required_exist &= RequireField(*result, ::onc::wifi::kEAP);
  else if (security == ::onc::wifi::kWEP_PSK ||
           security == ::onc::wifi::kWPA_PSK)
    all_required_exist &= RequireField(*result, ::onc::wifi::kPassphrase);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateVPN(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(
          *result, ::onc::vpn::kType,
          managed_onc_ ? GetValidManagedVPNTypes() : GetAllValidVPNTypes())) {
    return false;
  }

  bool all_required_exist = RequireField(*result, ::onc::vpn::kType);
  std::string type = GetStringFromDict(*result, ::onc::vpn::kType);
  if (type == ::onc::vpn::kOpenVPN) {
    all_required_exist &= RequireField(*result, ::onc::vpn::kOpenVPN);
  } else if (type == ::onc::vpn::kIPsec) {
    all_required_exist &= RequireField(*result, ::onc::vpn::kIPsec);
  } else if (type == ::onc::vpn::kTypeL2TP_IPsec) {
    all_required_exist &= RequireField(*result, ::onc::vpn::kIPsec) &&
                          RequireField(*result, ::onc::vpn::kL2TP);
  } else if (type == ::onc::vpn::kWireGuard) {
    all_required_exist &= RequireField(*result, ::onc::vpn::kWireGuard);
  } else if (type == ::onc::vpn::kThirdPartyVpn) {
    all_required_exist &= RequireField(*result, ::onc::vpn::kThirdPartyVpn);
  } else if (type == ::onc::vpn::kArcVpn) {
    all_required_exist &= RequireField(*result, ::onc::vpn::kArcVpn);
  }

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateIPsec(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::ipsec::kAuthenticationType,
                                    GetValidIPsecAuthentications()) ||
      FieldExistsAndIsEmpty(*result, ::onc::ipsec::kServerCARefs)) {
    return false;
  }

  if (!OnlyOneFieldSet(*result, ::onc::ipsec::kServerCARefs,
                       ::onc::ipsec::kServerCARef))
    return false;

  if (!ValidateClientCertFields(/*allow_cert_type_none=*/false, result))
    return false;

  bool all_required_exist =
      RequireField(*result, ::onc::ipsec::kAuthenticationType) &&
      RequireField(*result, ::onc::ipsec::kIKEVersion);
  std::string auth =
      GetStringFromDict(*result, ::onc::ipsec::kAuthenticationType);
  if (auth == ::onc::ipsec::kCert) {
    all_required_exist &=
        RequireField(*result, ::onc::client_cert::kClientCertType);
  }

  // For cert-based or EAP-based authentication, server CA must exist.
  // For PSK-based authentication, server CA must not exist.
  bool has_server_ca_cert = result->contains(::onc::ipsec::kServerCARefs) ||
                            result->contains(::onc::ipsec::kServerCARef) ||
                            result->contains(::onc::ipsec::kServerCAPEMs);
  if ((auth == ::onc::ipsec::kCert || auth == ::onc::ipsec::kEAP) &&
      !has_server_ca_cert) {
    all_required_exist = false;
    std::ostringstream msg;
    msg << "Server CA config is missing (one of the fields "
        << ::onc::ipsec::kServerCARefs << " or " << ::onc::ipsec::kServerCAPEMs
        << ").";
    AddValidationIssue(error_on_missing_field_, msg.str());
  }
  if (auth == ::onc::ipsec::kPSK && has_server_ca_cert) {
    std::ostringstream msg;
    msg << "Field '" << ::onc::ipsec::kServerCARefs << "' (or '"
        << ::onc::ipsec::kServerCARef << "') can only be set if '"
        << ::onc::ipsec::kAuthenticationType << "' is set to '"
        << ::onc::ipsec::kCert << "'.";
    AddValidationIssue(true /* is_error */, msg.str());
    return false;
  }

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateOpenVPN(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::openvpn::kAuthRetry,
                                    GetValidVPNAuthRetryValues()) ||
      FieldExistsAndHasNoValidValue(*result, ::onc::openvpn::kRemoteCertTLS,
                                    GetValidVPNCertTlsValues()) ||
      FieldExistsAndHasNoValidValue(*result,
                                    ::onc::openvpn::kCompressionAlgorithm,
                                    GetValidVPNCompressionAlgorithmValues()) ||
      FieldExistsAndHasNoValidValue(*result,
                                    ::onc::openvpn::kUserAuthenticationType,
                                    GetValidVPNUserAuthTypes()) ||
      FieldExistsAndIsEmpty(*result, ::onc::openvpn::kServerCARefs)) {
    return false;
  }

  // ONC policy prevents the UI from setting properties that are not explicitly
  // listed as 'recommended' (i.e. the default is 'enforced'). Historically
  // the configuration UI ignored this restriction. In order to support legacy
  // ONC configurations, add recommended entries for user authentication
  // properties where appropriate.
  if ((onc_source_ == ::onc::ONC_SOURCE_DEVICE_POLICY ||
       onc_source_ == ::onc::ONC_SOURCE_USER_POLICY)) {
    base::Value::List* recommended = result->FindList(::onc::kRecommended);
    if (!recommended) {
      recommended =
          &result->Set(::onc::kRecommended, base::Value::List())->GetList();
    }

    // If kUserAuthenticationType is unspecified, allow Password and OTP.
    if (!result->FindString(::onc::openvpn::kUserAuthenticationType)) {
      AddKeyToList(::onc::openvpn::kPassword, recommended);
      AddKeyToList(::onc::openvpn::kOTP, recommended);
    }

    // If client cert type is not provided, empty, or 'None', allow client cert
    // properties.
    std::string client_cert_type =
        GetStringFromDict(*result, ::onc::client_cert::kClientCertType);
    if (client_cert_type.empty() ||
        client_cert_type == ::onc::client_cert::kClientCertTypeNone) {
      AddKeyToList(::onc::client_cert::kClientCertType, recommended);
      AddKeyToList(::onc::client_cert::kClientCertPKCS11Id, recommended);
    }
  }

  if (!OnlyOneFieldSet(*result, ::onc::openvpn::kServerCARefs,
                       ::onc::openvpn::kServerCARef))
    return false;

  if (!ValidateClientCertFields(true /* allow ClientCertType None */, result))
    return false;

  bool all_required_exist =
      RequireField(*result, ::onc::client_cert::kClientCertType);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateWireGuard(base::Value::Dict* result) {
  const base::Value::List* peers = result->FindList(::onc::wireguard::kPeers);
  std::ostringstream msg;
  if (!peers) {
    msg << "A " << ::onc::wireguard::kPeers
        << " list is required but not present.";
    AddValidationIssue(true /* is_error */, msg.str());
    return false;
  }
  for (const base::Value& p : *peers) {
    if (!p.GetDict().contains(::onc::wireguard::kPublicKey)) {
      msg << ::onc::wireguard::kPublicKey
          << " field is required for each peer.";
      AddValidationIssue(true /* is_error */, msg.str());
      return false;
    }
  }

  const bool all_required_exist =
      RequireField(*result, ::onc::wireguard::kEndpoint);
  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateThirdPartyVPN(base::Value::Dict* result) {
  const bool all_required_exist =
      RequireField(*result, ::onc::third_party_vpn::kExtensionID);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateARCVPN(base::Value::Dict* result) {
  return true;
}

bool Validator::ValidateVerifyX509(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::verify_x509::kType,
                                    GetValidX509Types())) {
    return false;
  }

  bool all_required_exist = RequireField(*result, ::onc::verify_x509::kName);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateCertificatePattern(base::Value::Dict* result) {
  bool all_required_exist = true;
  if (!result->contains(::onc::client_cert::kSubject) &&
      !result->contains(::onc::client_cert::kIssuer) &&
      !result->contains(::onc::client_cert::kIssuerCARef)) {
    all_required_exist = false;
    std::ostringstream msg;
    msg << "None of the fields '" << ::onc::client_cert::kSubject << "', '"
        << ::onc::client_cert::kIssuer << "', and '"
        << ::onc::client_cert::kIssuerCARef
        << "' is present, but at least one is required.";
    AddValidationIssue(error_on_missing_field_, msg.str());
  }

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateGlobalNetworkConfiguration(base::Value::Dict* result) {
  // Replace the deprecated kBlacklistedHexSSIDs with kBlockedHexSSIDs.
  if (!result->contains(::onc::global_network_config::kBlockedHexSSIDs)) {
    std::optional<base::Value> blocked =
        result->Extract(::onc::global_network_config::kBlacklistedHexSSIDs);
    if (blocked) {
      result->Set(::onc::global_network_config::kBlockedHexSSIDs,
                  std::move(*blocked));
    }
  }

  // Validate that these are only allowed in device policy.
  const std::string_view kDevicePolicyOnlyKeys[] = {
      ::onc::global_network_config::kAllowTextMessages,
      ::onc::global_network_config::kAllowCellularSimLock,
      ::onc::global_network_config::kAllowCellularHotspot,
      ::onc::global_network_config::kAllowAPNModification,
      ::onc::global_network_config::kDisableNetworkTypes,
      ::onc::global_network_config::kAllowOnlyPolicyCellularNetworks,
      ::onc::global_network_config::kAllowOnlyPolicyWiFiToConnect,
      ::onc::global_network_config::kAllowOnlyPolicyWiFiToConnectIfAvailable,
      ::onc::global_network_config::kBlockedHexSSIDs,
      ::onc::global_network_config::kRecommendedValuesAreEphemeral,
      ::onc::global_network_config::
          kUserCreatedNetworkConfigurationsAreEphemeral,
      ::onc::global_network_config::kDisconnectWiFiOnEthernet};
  for (std::string_view key : kDevicePolicyOnlyKeys) {
    if (!IsInDevicePolicy(result, key)) {
      return false;
    }
  }

  std::vector<const char*> valid_network_types = GetValidNetworkTypes();
  valid_network_types.push_back(::onc::network_config::kWimaxDeprecated);

  // Ensure the list contains only legitimate network type identifiers.
  if (!ListFieldContainsValidValues(
          *result, ::onc::global_network_config::kDisableNetworkTypes,
          valid_network_types)) {
    return false;
  }

  // Ensure that AllowTextMessages contains valid types
  if (FieldExistsAndHasNoValidValue(
          *result, ::onc::global_network_config::kAllowTextMessages,
          GetValidAllowTextMessagesTypes())) {
    return false;
  }

  if (!CheckAdminAssignedAPNIdsAreNonEmptyAndAddToSet(
          *result, ::onc::global_network_config::kPSIMAdminAssignedAPNIds)) {
    return false;
  }

  return true;
}

bool Validator::ValidateProxySettings(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::proxy::kType,
                                    GetValidProxySettingsTypes())) {
    return false;
  }

  bool all_required_exist = RequireField(*result, ::onc::proxy::kType);
  std::string type = GetStringFromDict(*result, ::onc::proxy::kType);
  if (type == ::onc::proxy::kManual)
    all_required_exist &= RequireField(*result, ::onc::proxy::kManual);
  else if (type == ::onc::proxy::kPAC)
    all_required_exist &= RequireField(*result, ::onc::proxy::kPAC);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateProxyLocation(base::Value::Dict* result) {
  bool all_required_exist = RequireField(*result, ::onc::proxy::kHost) &&
                            RequireField(*result, ::onc::proxy::kPort);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateEAP(base::Value::Dict* result) {
  // If this EAP dict is in a IPsec dict (i.e., IPsec is the second-to-last
  // element in its path), the only valid method is MSCHAPv2.
  std::vector<const char*> valid_outer_values = GetValidEAPOuterValues();
  if (path_.size() >= 2) {
    auto it = std::next(path_.rbegin());
    if (*it == ::onc::vpn::kIPsec)
      valid_outer_values = {::onc::eap::kMSCHAPv2};
  }

  if (FieldExistsAndHasNoValidValue(*result, ::onc::eap::kInner,
                                    GetValidEAPInnerValues()) ||
      FieldExistsAndHasNoValidValue(*result, ::onc::eap::kOuter,
                                    valid_outer_values) ||
      FieldExistsAndIsEmpty(*result, ::onc::eap::kServerCARefs)) {
    return false;
  }

  if (!OnlyOneFieldSet(*result, ::onc::eap::kServerCARefs,
                       ::onc::eap::kServerCARef))
    return false;

  if (!ValidateClientCertFields(true /* allow ClientCertType None */, result))
    return false;

  bool all_required_exist = RequireField(*result, ::onc::eap::kOuter);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateSubjectAlternativeNameMatch(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(
          *result, ::onc::eap_subject_alternative_name_match::kType,
          GetValidEAPSubjectAlternativeNameMatchTypes())) {
    return false;
  }

  bool all_required_exist =
      RequireField(*result, ::onc::eap_subject_alternative_name_match::kType) &&
      RequireField(*result, ::onc::eap_subject_alternative_name_match::kValue);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateCertificate(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::certificate::kType,
                                    GetValidCertificateTypes()) ||
      FieldExistsAndIsEmpty(*result, ::onc::certificate::kGUID)) {
    return false;
  }

  std::string type = GetStringFromDict(*result, ::onc::certificate::kType);

  if (!CheckGuidIsUniqueAndAddToSet(*result, ::onc::certificate::kGUID,
                                    &certificate_guids_))
    return false;

  bool all_required_exist = RequireField(*result, ::onc::certificate::kGUID);

  bool remove = result->FindBool(::onc::kRemove).value_or(false);
  if (remove) {
    path_.push_back(::onc::kRemove);
    std::ostringstream msg;
    msg << "Removal of certificates is not supported.";
    AddValidationIssue(true /* is_error */, msg.str());
    path_.pop_back();
    return false;
  }

  all_required_exist &= RequireField(*result, ::onc::certificate::kType);

  if (type == ::onc::certificate::kClient)
    all_required_exist &= RequireField(*result, ::onc::certificate::kPKCS12);
  else if (type == ::onc::certificate::kServer ||
           type == ::onc::certificate::kAuthority)
    all_required_exist &= RequireField(*result, ::onc::certificate::kX509);

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateScope(base::Value::Dict* result) {
  if (FieldExistsAndHasNoValidValue(*result, ::onc::scope::kType,
                                    GetValidScopeTypes()) ||
      FieldExistsAndIsEmpty(*result, ::onc::scope::kId)) {
    return false;
  }

  bool all_required_exist = RequireField(*result, ::onc::scope::kType);
  const std::string* type_string = result->FindString(::onc::scope::kType);
  if (type_string && *type_string == ::onc::scope::kExtension) {
    all_required_exist &= RequireField(*result, ::onc::scope::kId);
    // Check Id validity for type 'Extension'.
    const std::string* id_string = result->FindString(::onc::scope::kId);
    if (id_string && !crx_file::id_util::IdIsValid(*id_string)) {
      std::ostringstream msg;
      msg << "Field '" << ::onc::scope::kId << "' is not a valid extension id.";
      AddValidationIssue(false /* is_error */, msg.str());
      return false;
    }
  }

  return !error_on_missing_field_ || all_required_exist;
}

bool Validator::ValidateTether(base::Value::Dict* result) {
  if (FieldExistsAndIsNotInRange(*result, ::onc::tether::kBatteryPercentage, 0,
                                 100) ||
      FieldExistsAndIsNotInRange(*result, ::onc::tether::kSignalStrength, 0,
                                 100) ||
      FieldExistsAndIsEmpty(*result, ::onc::tether::kCarrier)) {
    return false;
  }

  bool all_required_exist =
      RequireField(*result, ::onc::tether::kHasConnectedToHost);
  all_required_exist &=
      RequireField(*result, ::onc::tether::kBatteryPercentage);
  all_required_exist &= RequireField(*result, ::onc::tether::kSignalStrength);
  all_required_exist &= RequireField(*result, ::onc::tether::kCarrier);

  return !error_on_missing_field_ || all_required_exist;
}

void Validator::ValidateEthernetConfigs(
    base::Value::List* network_configurations_list) {
  // Ensures that at most one NetworkConfiguration is effective within these
  // categories:
  // - "Type": "Ethernet" and "Authentication": "None"
  // - "Type": "Ethernet" and "Authentication": "8021X"
  // This is currently necessary because shill only persists one configuration
  // per such category and the UI only supports one Ethernet configuration.
  // TODO(b/159725895): Design better Ethernet configuration + policy
  // management.
  std::vector<std::string> ethernet_auth_none_guids;
  std::vector<std::string> ethernet_auth_8021x_guids;

  for (const base::Value& network_configuration :
       *network_configurations_list) {
    const std::string* guid = network_configuration.GetDict().FindString(
        ::onc::network_config::kGUID);
    const base::Value::Dict* ethernet =
        network_configuration.GetDict().FindDict(
            ::onc::network_config::kEthernet);
    if (!guid || !ethernet)
      continue;

    const std::string* auth =
        ethernet->FindString(::onc::ethernet::kAuthentication);
    if (!auth)
      continue;
    if (*auth == ::onc::ethernet::kAuthenticationNone)
      ethernet_auth_none_guids.push_back(*guid);
    if (*auth == ::onc::ethernet::k8021X)
      ethernet_auth_8021x_guids.push_back(*guid);
  }

  // If there were multiple NetworkConfigurations in such a bucket, keep the
  // last one because that's the one which would be effective, as it would be
  // applies last in shill.
  OnlyKeepLast(network_configurations_list, ethernet_auth_none_guids,
               /*type=*/"Ethernet");
  OnlyKeepLast(network_configurations_list, ethernet_auth_8021x_guids,
               /*type=*/"Ethernet 802.1x");
}

void Validator::OnlyKeepLast(base::Value::List* network_configurations_list,
                             const std::vector<std::string>& guids,
                             const char* type_for_messages) {
  if (guids.size() < 2)
    return;
  for (size_t i = 0; i < guids.size() - 1; ++i) {
    RemoveNetworkConfigurationWithGuid(network_configurations_list, guids[i]);

    std::ostringstream msg;
    msg << "NetworkConfiguration '" << guids[i] << "' ignored - only one "
        << type_for_messages << " configuration can be processed";
    AddValidationIssue(/*is_error=*/false, msg.str());
  }
}

void Validator::RemoveNetworkConfigurationWithGuid(
    base::Value::List* network_configurations_list,
    const std::string& guid_to_remove) {
  base::Value::List& list = *network_configurations_list;
  for (auto it = list.begin(); it != list.end(); ++it) {
    const std::string* guid =
        it->GetDict().FindString(::onc::network_config::kGUID);
    if (!guid)
      continue;
    if (*guid == guid_to_remove) {
      list.erase(it);
      return;
    }
  }
}

void Validator::AddValidationIssue(bool is_error,
                                   const std::string& debug_info) {
  std::ostringstream msg;
  msg << (is_error ? "ERROR: " : "WARNING: ") << debug_info << " (at "
      << (path_.empty() ? "toplevel" : base::JoinString(path_, ".")) << ")";
  std::string message = msg.str();

  if (is_error)
    NET_LOG(ERROR) << message;
  else if (log_warnings_)
    NET_LOG(DEBUG) << message;

  validation_issues_.push_back({is_error, message});
}

}  // namespace chromeos::onc