Skip to content

Commit

Permalink
OCPP 1.6 add schema validation checks on updates to custom keys (#853)
Browse files Browse the repository at this point in the history
* fix: add schema validation checks on updates to custom keys

There was a missing check on receipt of ChangeConfiguration.req for
custom keys where a value could be set that would fail schema validation
and prevent EVerest from successfully restarting.

This update checks the new value before updating.
Validation failures are noitfied to the CSMS via the ChangeConfiguration.conf

Signed-off-by: James Chapman <[email protected]>

* fix: added license header

Signed-off-by: James Chapman <[email protected]>

---------

Signed-off-by: James Chapman <[email protected]>
  • Loading branch information
james-ctc authored Nov 6, 2024
1 parent b1f947e commit d41bfc1
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 7 deletions.
4 changes: 4 additions & 0 deletions include/ocpp/common/schemas.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class Schemas {
public:
/// \brief Creates a new Schemas object looking for the root schema file in relation to the provided \p main_dir
explicit Schemas(fs::path schemas_path);
/// \brief Creates a new Schemas object using the supplied JSON schema
explicit Schemas(const json& schema_in);
/// \brief Creates a new Schemas object using the supplied JSON schema
explicit Schemas(json&& schema_in);

/// \brief Provides the config schema
/// \returns the config schema as as json object
Expand Down
12 changes: 12 additions & 0 deletions lib/ocpp/common/schemas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ Schemas::Schemas(fs::path schemas_path) : schemas_path(schemas_path) {
}
}

Schemas::Schemas(const json& schema_in) : schema(schema_in) {
validator = std::make_shared<json_validator>(
[this](const json_uri& uri, json& schema) { this->loader(uri, schema); }, Schemas::format_checker);
validator->set_root_schema(this->schema);
}

Schemas::Schemas(json&& schema_in) : schema(std::move(schema_in)) {
validator = std::make_shared<json_validator>(
[this](const json_uri& uri, json& schema) { this->loader(uri, schema); }, Schemas::format_checker);
validator->set_root_schema(this->schema);
}

void Schemas::load_root_schema() {
fs::path config_schema_path = this->schemas_path / "Config.json";

Expand Down
20 changes: 14 additions & 6 deletions lib/ocpp/v16/charge_point_configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2858,20 +2858,28 @@ ConfigurationStatus ChargePointConfiguration::setCustomKey(CiString<50> key, CiS
}
std::lock_guard<std::recursive_mutex> lock(this->configuration_mutex);
try {
const auto type = this->custom_schema["properties"][key]["type"];
const auto type = custom_schema["properties"][key]["type"];
json new_value;
if (type == "integer") {
this->config["Custom"][key] = std::stoi(value.get());
new_value = std::stoi(value.get());
} else if (type == "number") {
this->config["Custom"][key] = std::stof(value.get());
new_value = std::stof(value.get());
} else if (type == "string" or type == "array") {
this->config["Custom"][key] = value.get();
new_value = value.get();
} else if (type == "boolean") {
this->config["Custom"][key] = ocpp::conversions::string_to_bool(value.get());
new_value = ocpp::conversions::string_to_bool(value.get());
} else {
return ConfigurationStatus::Rejected;
}

// validate the updated key against the schema
Schemas schema(custom_schema);
json model;
model[key] = new_value;
schema.get_validator()->validate(model); // throws exception on error
config["Custom"][key] = new_value;
} catch (const std::exception& e) {
EVLOG_warning << "Could not set custom configuration key";
EVLOG_warning << "Could not set custom configuration key: " << e.what();
return ConfigurationStatus::Rejected;
}

Expand Down
3 changes: 2 additions & 1 deletion tests/lib/ocpp/v16/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
target_include_directories(libocpp_unit_tests PUBLIC
target_include_directories(libocpp_unit_tests PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)

Expand All @@ -13,6 +13,7 @@ target_sources(libocpp_unit_tests PRIVATE
test_message_queue.cpp
test_charge_point_state_machine.cpp
test_composite_schedule.cpp
test_config_validation.cpp
)

# Copy the json files used for testing to the destination directory
Expand Down
126 changes: 126 additions & 0 deletions tests/lib/ocpp/v16/test_config_validation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@

// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest

#include <gtest/gtest.h>

#include <everest/logging.hpp>
#include <ocpp/common/schemas.hpp>

#include <memory>
#include <nlohmann/json-schema.hpp>
#include <nlohmann/json.hpp>

namespace {
using nlohmann::basic_json;
using nlohmann::json;
using nlohmann::json_uri;
using nlohmann::json_schema::basic_error_handler;
using nlohmann::json_schema::json_validator;

constexpr const char* test_schema = R"({
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Json schema for Custom configuration keys",
"$comment": "This is just an example schema and can be modified according to custom requirements",
"type": "object",
"required": [],
"properties": {
"ConnectorType": {
"type": "string",
"enum": [
"cType2",
"sType2"
],
"default": "sType2",
"description": "Used to indicate the type of connector used by the unit",
"readOnly": true
},
"ConfigLastUpdatedBy": {
"type": "array",
"items": {
"type": "string",
"enum": [
"LOCAL",
"CPMS"
]
},
"description": "Variable used to indicate how the Charge Points configuration was last updated",
"readOnly": true
}
}
})";

class SchemaTest : public testing::Test {
static void format_checker(const std::string& format, const std::string& value) {
EVLOG_error << "format_checker: '" << format << "' '" << value << '\'';
}

static void loader(const json_uri& uri, json& schema) {
schema = nlohmann::json_schema::draft7_schema_builtin;
}

class custom_error_handler : public basic_error_handler {
private:
void error(const json::json_pointer& pointer, const json& instance, const std::string& message) override {
basic_error_handler::error(pointer, instance, message);
EVLOG_error << "'" << pointer << "' - '" << instance << "': " << message;
errors = true;
}

public:
bool errors{false};
constexpr bool has_errors() const {
return errors;
}
};

protected:
std::unique_ptr<json_validator> validator;
json schema;
custom_error_handler err;

void SetUp() override {
schema = json::parse(test_schema);
validator = std::make_unique<json_validator>(&loader, &format_checker);
validator->set_root_schema(schema);
err.errors = false;
}
};

TEST_F(SchemaTest, ValidationText) {
json model = R"({"ConnectorType":"cType2"})"_json;
validator->validate(model, err);
EXPECT_FALSE(err.has_errors());
}

TEST_F(SchemaTest, ValidationObj) {
json model;
model["ConnectorType"] = "cType2";
validator->validate(model, err);
EXPECT_FALSE(err.has_errors());
}

TEST_F(SchemaTest, ValidationObjErr) {
json model;
model["ConnectorType"] = "cType3";
validator->validate(model, err);
EXPECT_TRUE(err.has_errors());
}

TEST(SchemaObj, Success) {
ocpp::Schemas schema(std::move(json::parse(test_schema)));
auto validator = schema.get_validator();
json model;
model["ConnectorType"] = "cType2";
EXPECT_NO_THROW(validator->validate(model));
}

TEST(SchemaObj, Fail) {
ocpp::Schemas schema(std::move(json::parse(test_schema)));
auto validator = schema.get_validator();
json model;
model["ConnectorType"] = "cType3";
EXPECT_ANY_THROW(validator->validate(model));
}

} // namespace

0 comments on commit d41bfc1

Please sign in to comment.