blob: 2ef42a17e0a5a3a1ee87b82a1b573e2153e2365c [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Test support library for request payloads.
#include "chrome/browser/policy/messaging_layer/util/test_request_payload.h"
#include <algorithm>
#include <string>
#include <string_view>
#include "base/json/json_reader.h"
#include "base/strings/string_util.h"
#include "chrome/browser/policy/messaging_layer/upload/encrypted_reporting_client.h"
#include "chrome/browser/policy/messaging_layer/upload/record_upload_request_builder.h"
#include "components/reporting/util/encrypted_reporting_json_keys.h"
#include "third_party/abseil-cpp/absl/strings/ascii.h"
namespace reporting {
// Return true if s a properly formatted positive integer, i.e., is not empty,
// contains digits only and does not start with 0.
static bool IsPositiveInteger(std::string_view s) {
if (s.empty()) {
return false;
}
if (s.size() == 1) {
return absl::ascii_isdigit(static_cast<unsigned char>(s[0]));
}
return s[0] != '0' && s.find_first_not_of("0123456789") == std::string::npos;
}
// Get the record list. If it can't, print the message to listener and return a
// null pointer.
static const base::Value::List* GetRecordList(const base::Value::Dict& arg,
MatchResultListener* listener) {
const auto* const record_list = arg.FindList(json_keys::kEncryptedRecordList);
if (record_list == nullptr) {
*listener << "No key named \"encryptedRecord\" in the argument or the "
"value is not a list.";
return nullptr;
}
return record_list;
}
bool AttachEncryptionSettingsMatcher::MatchAndExplain(
const base::Value::Dict& arg,
MatchResultListener* listener) const {
const auto attach_encryption_settings =
arg.FindBool(json_keys::kAttachEncryptionSettings);
if (!attach_encryption_settings) {
*listener << "No key named \"attachEncryptionSettings\" in the argument or "
"the value is not of bool type.";
return false;
}
if (!attach_encryption_settings.value()) {
*listener << "The value of \"attachEncryptionSettings\" is false.";
return false;
}
return true;
}
void AttachEncryptionSettingsMatcher::DescribeTo(std::ostream* os) const {
*os << "has a valid attachEncryptionSettings field.";
}
void AttachEncryptionSettingsMatcher::DescribeNegationTo(
std::ostream* os) const {
*os << "has an invalid attachEncryptionSettings field.";
}
std::string AttachEncryptionSettingsMatcher::Name() const {
return "attach-encryption-settings-matcher";
}
bool NoAttachEncryptionSettingsMatcher::MatchAndExplain(
const base::Value::Dict& arg,
MatchResultListener* listener) const {
if (arg.Find(json_keys::kAttachEncryptionSettings) != nullptr) {
*listener << "Found \"attachEncryptionSettings\" in the argument.";
return false;
}
return true;
}
void NoAttachEncryptionSettingsMatcher::DescribeTo(std::ostream* os) const {
*os << "expectedly has no attachEncryptionSettings field.";
}
void NoAttachEncryptionSettingsMatcher::DescribeNegationTo(
std::ostream* os) const {
*os << "unexpectedly has an attachEncryptionSettings field.";
}
std::string NoAttachEncryptionSettingsMatcher::Name() const {
return "no-attach-encryption-settings-matcher";
}
bool ConfigurationFileVersionMatcher::MatchAndExplain(
const base::Value::Dict& arg,
MatchResultListener* listener) const {
auto* attach_configuration_file =
arg.Find(json_keys::kConfigurationFileVersion);
if (!attach_configuration_file->GetIfInt().has_value()) {
*listener << "No key named \"configurationFileVersion\" in the argument or "
"the value is not of int type.";
return false;
}
return true;
}
void ConfigurationFileVersionMatcher::DescribeTo(std::ostream* os) const {
*os << "has a valid configurationFileVersion field.";
}
void ConfigurationFileVersionMatcher::DescribeNegationTo(
std::ostream* os) const {
*os << "has an invalid configurationFileVersion field.";
}
std::string ConfigurationFileVersionMatcher::Name() const {
return "configuration-file-version-matcher";
}
bool NoConfigurationFileVersionMatcher::MatchAndExplain(
const base::Value::Dict& arg,
MatchResultListener* listener) const {
if (arg.Find(json_keys::kConfigurationFileVersion) != nullptr) {
*listener << "Found \"configurationFileVersion\" in the argument.";
return false;
}
return true;
}
void NoConfigurationFileVersionMatcher::DescribeTo(std::ostream* os) const {
*os << "expectedly has no configurationFileVersion field.";
}
void NoConfigurationFileVersionMatcher::DescribeNegationTo(
std::ostream* os) const {
*os << "unexpectedly has an configurationFileVersion field.";
}
std::string NoConfigurationFileVersionMatcher::Name() const {
return "no-configuration-file-version-matcher";
}
bool SourceMatcher::MatchAndExplain(const base::Value::Dict& arg,
MatchResultListener* listener) const {
if (arg.FindString(json_keys::kSource) == nullptr) {
*listener << "No key named \"source\" or the value "
"is not a string in the argument.";
return false;
}
return true;
}
void SourceMatcher::DescribeTo(std::ostream* os) const {
*os << "has a valid source field.";
}
void SourceMatcher::DescribeNegationTo(std::ostream* os) const {
*os << "has an invalid source field.";
}
std::string SourceMatcher::Name() const {
return "source-test-matcher";
}
bool NoSourceMatcher::MatchAndExplain(const base::Value::Dict& arg,
MatchResultListener* listener) const {
if (arg.Find(json_keys::kSource) != nullptr) {
*listener << "Found \"source\" in the argument.";
return false;
}
return true;
}
void NoSourceMatcher::DescribeTo(std::ostream* os) const {
*os << "expectedly has no source field.";
}
void NoSourceMatcher::DescribeNegationTo(std::ostream* os) const {
*os << "unexpectedly has an Source field.";
}
std::string NoSourceMatcher::Name() const {
return "source-test-matcher";
}
void CompressionInformationMatcher::DescribeTo(std::ostream* os) const {
*os << "has a valid compression information field.";
}
void CompressionInformationMatcher::DescribeNegationTo(std::ostream* os) const {
*os << "has an invalid compression information field.";
}
std::string CompressionInformationMatcher::Name() const {
return "encrypted-record-matcher";
}
bool EncryptedRecordMatcher::MatchAndExplain(
const base::Value::Dict& arg,
MatchResultListener* listener) const {
const auto* const record_list = GetRecordList(arg, listener);
return record_list != nullptr;
}
void EncryptedRecordMatcher::DescribeTo(std::ostream* os) const {
*os << "has a valid encryptedRecord field.";
}
void EncryptedRecordMatcher::DescribeNegationTo(std::ostream* os) const {
*os << "has an invalid encryptedRecord field.";
}
std::string EncryptedRecordMatcher::Name() const {
return "encrypted-record-matcher";
}
bool RequestIdMatcher::MatchAndExplain(const base::Value::Dict& arg,
MatchResultListener* listener) const {
const auto* const request_id = arg.FindString(json_keys::kRequestId);
if (request_id == nullptr) {
*listener << "No key named \"requestId\" in the argument or the value "
"is not a string.";
return false;
}
if (request_id->empty()) {
*listener << "Request ID is empty.";
return false;
}
if (!std::ranges::all_of(*request_id, base::IsHexDigit<char>)) {
*listener << "Request ID is not a hexadecimal number.";
return false;
}
return true;
}
void RequestIdMatcher::DescribeTo(std::ostream* os) const {
*os << "has a valid request ID.";
}
void RequestIdMatcher::DescribeNegationTo(std::ostream* os) const {
*os << "has an invalid request ID.";
}
std::string RequestIdMatcher::Name() const {
return "request-id-matcher";
}
bool RecordMatcher::MatchAndExplain(const base::Value::Dict& arg,
MatchResultListener* listener) const {
switch (mode_) {
case Mode::FullRequest: {
const auto* record_list = GetRecordList(arg, listener);
if (record_list == nullptr) {
return false;
}
for (const auto& record : *record_list) {
const auto* record_dict = record.GetIfDict();
if (record_dict == nullptr) {
*listener << "Record " << record << " is not a dict.";
return false;
}
if (!this->MatchAndExplainRecord(*record_dict, listener)) {
return false;
}
}
return true;
}
case Mode::RecordOnly: {
return this->MatchAndExplainRecord(arg, listener);
}
default: {
// Should never reach here.
*listener << "Invalid mode of RecordMatcher encountered: "
<< static_cast<std::underlying_type_t<Mode>>(mode_);
return false;
}
}
}
RecordMatcher& RecordMatcher::SetMode(RecordMatcher::Mode mode) {
mode_ = mode;
return *this;
}
bool CompressionInformationMatcher::MatchAndExplainRecord(
const base::Value::Dict& record,
MatchResultListener* listener) const {
const auto* const compression_info =
record.FindDict(json_keys::kCompressionInformation);
if (compression_info == nullptr) {
*listener << "No key named \"compressionInformation\" or the value is "
"not a dict in record "
<< record << '.';
return false;
}
const auto compression_algorithm =
compression_info->FindInt(json_keys::kCompressionAlgorithm);
if (!compression_algorithm.has_value()) {
*listener << "No key named \"compressionAlgorithm\" under "
"compressionInformation "
"or the value is not an integer in record "
<< record << '.';
return false;
}
if (compression_algorithm.value() > 1 || compression_algorithm < 0) {
*listener << "The value of \"compressionInformation/compressionAlgorithm\" "
"is neither 0 nor 1 in record "
<< record << '.';
return false;
}
return true;
}
bool EncryptedWrappedRecordRecordMatcher::MatchAndExplainRecord(
const base::Value::Dict& record,
MatchResultListener* listener) const {
if (record.FindString(json_keys::kEncryptedWrappedRecord) == nullptr) {
*listener << "No key named \"encryptedWrappedRecord\" or the value "
"is not a string in record "
<< record << '.';
return false;
}
return true;
}
void EncryptedWrappedRecordRecordMatcher::DescribeTo(std::ostream* os) const {
*os << "has valid encrypted wrapped records.";
}
void EncryptedWrappedRecordRecordMatcher::DescribeNegationTo(
std::ostream* os) const {
*os << "has at least one invalid encrypted wrapped records or has missing "
"encrypted wrapped records.";
}
std::string EncryptedWrappedRecordRecordMatcher::Name() const {
return "encrypted-wrapped-record-record-matcher";
}
bool NoEncryptedWrappedRecordRecordMatcher::MatchAndExplainRecord(
const base::Value::Dict& record,
MatchResultListener* listener) const {
if (record.Find(json_keys::kEncryptedWrappedRecord) != nullptr) {
*listener << "Found \"encryptedWrappedRecord\" in record " << record << '.';
return false;
}
return true;
}
void NoEncryptedWrappedRecordRecordMatcher::DescribeTo(std::ostream* os) const {
*os << "expectedly has no encrypted wrapped record.";
}
void NoEncryptedWrappedRecordRecordMatcher::DescribeNegationTo(
std::ostream* os) const {
*os << "has at least one encrypted wrapped record that it should not have.";
}
std::string NoEncryptedWrappedRecordRecordMatcher::Name() const {
return "no-encrypted-wrapped-record-record-matcher";
}
bool SequenceInformationRecordMatcher::MatchAndExplainRecord(
const base::Value::Dict& record,
MatchResultListener* listener) const {
const auto* const sequence_information =
record.FindDict(json_keys::kSequenceInformation);
if (sequence_information == nullptr) {
*listener << "No key named \"sequenceInformation\" or the value is "
"not a dict in record "
<< record << '.';
return false;
}
if (!sequence_information->FindInt(json_keys::kPriority).has_value()) {
*listener << "No key named \"sequenceInformation/priority\" or the "
"value is not an integer in record "
<< record << '.';
return false;
}
for (const char* id : {json_keys::kSequencingId, json_keys::kGenerationId}) {
const auto* const id_val = sequence_information->FindString(id);
if (id_val == nullptr) {
*listener << "No key named \"sequenceInformation/" << id
<< "\" or the value is not a string in record " << record
<< '.';
return false;
}
if (!IsPositiveInteger(*id_val)) {
*listener << "The value of \"sequenceInformation/" << id
<< "\" is not a properly formatted positive integer in record "
<< record << '.';
return false;
}
}
#if BUILDFLAG(IS_CHROMEOS)
if (EncryptedReportingClient::GenerationGuidIsRequired()) {
const auto* generation_guid =
sequence_information->FindString(json_keys::kGenerationGuid);
if ((!generation_guid || generation_guid->empty())) {
*listener << "No key named \"sequenceInformation/generationGuid\" or the "
"value is not a string in record "
<< record << '.';
return false;
}
}
#endif // BUILDFLAG(IS_CHROMEOS)
return true;
}
void SequenceInformationRecordMatcher::DescribeTo(std::ostream* os) const {
*os << "has valid sequence information.";
}
void SequenceInformationRecordMatcher::DescribeNegationTo(
std::ostream* os) const {
*os << "has invalid sequence information.";
}
std::string SequenceInformationRecordMatcher::Name() const {
return "sequence-information-record-matcher";
}
bool RequestContainingRecordMatcher::IsSubDict(const base::Value::Dict& sub,
const base::Value::Dict& super) {
for (auto&& [key, sub_value] : sub) {
const auto* super_value = super.Find(key);
if (super_value == nullptr || *super_value != sub_value) {
return false;
}
}
return true;
}
RequestContainingRecordMatcher::RequestContainingRecordMatcher(
std::string_view matched_record_json)
: matched_record_json_(matched_record_json) {}
bool RequestContainingRecordMatcher::MatchAndExplain(
const base::Value::Dict& arg,
MatchResultListener* listener) const {
const auto* record_list = GetRecordList(arg, listener);
if (record_list == nullptr) {
return false;
}
const auto matched_record = base::JSONReader::Read(matched_record_json_);
if (!matched_record.has_value()) {
*listener << "The specified record cannot be parsed as a JSON object.";
return false;
}
const auto* matched_record_dict = matched_record->GetIfDict();
if (matched_record_dict == nullptr) {
*listener
<< "The specified record must be a Dict itself because each record "
"is a Dict.";
return false;
}
for (const auto& record : *record_list) {
const auto* record_dict = record.GetIfDict();
if (!record_dict) {
continue;
}
// Match each key and value of matched_record with those of the iterated
// record_dict. In this way, users can specify part of a record instead
// of the full record.
if (IsSubDict(*matched_record_dict, *record_dict)) {
return true;
}
}
*listener << "The specified record is not found in the argument.";
return false;
}
void RequestContainingRecordMatcher::DescribeTo(std::ostream* os) const {
*os << "contains the specified record.";
}
void RequestContainingRecordMatcher::DescribeNegationTo(
std::ostream* os) const {
*os << "does not contain the specified record or there are other failed "
"conditions.";
}
} // namespace reporting