Merge pull request #24711 from yashykt/filewatcherconfig

File watcher certificate provider config parsing
reviewable/pr24618/r8
Yash Tibrewal 4 years ago committed by GitHub
commit 0b4c258354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      BUILD
  2. 2
      BUILD.gn
  3. 41
      CMakeLists.txt
  4. 2
      Makefile
  5. 15
      build_autogenerated.yaml
  6. 1
      config.m4
  7. 1
      config.w32
  8. 2
      gRPC-C++.podspec
  9. 3
      gRPC-Core.podspec
  10. 2
      grpc.gemspec
  11. 1
      grpc.gyp
  12. 2
      package.xml
  13. 38
      src/core/ext/filters/client_channel/resolver_result_parsing.cc
  14. 119
      src/core/ext/xds/file_watcher_certificate_provider_factory.cc
  15. 72
      src/core/ext/xds/file_watcher_certificate_provider_factory.h
  16. 157
      src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc
  17. 167
      src/core/lib/json/json_util.h
  18. 1
      src/python/grpcio/grpc_core_dependencies.py
  19. 9
      test/core/client_channel/service_config_test.cc
  20. 12
      test/core/xds/BUILD
  21. 202
      test/core/xds/file_watcher_certificate_provider_factory_test.cc
  22. 2
      tools/doxygen/Doxyfile.c++.internal
  23. 2
      tools/doxygen/Doxyfile.core.internal
  24. 24
      tools/run_tests/generated/tests.json

16
BUILD

@ -1384,12 +1384,28 @@ grpc_cc_library(
"envoy_ads_upbdefs",
"grpc_base",
"grpc_client_channel",
"grpc_file_watcher_certificate_provider_factory",
"grpc_google_mesh_ca_certificate_provider_factory",
"grpc_transport_chttp2_client_secure",
"grpc_xds_credentials",
],
)
grpc_cc_library(
name = "grpc_file_watcher_certificate_provider_factory",
srcs = [
"src/core/ext/xds/file_watcher_certificate_provider_factory.cc",
],
hdrs = [
"src/core/ext/xds/file_watcher_certificate_provider_factory.h",
],
language = "c++",
deps = [
"grpc_base",
"grpc_xds_credentials",
],
)
grpc_cc_library(
name = "grpc_google_mesh_ca_certificate_provider_factory",
srcs = [

@ -728,6 +728,8 @@ config("grpc_config") {
"src/core/ext/xds/certificate_provider_registry.h",
"src/core/ext/xds/certificate_provider_store.cc",
"src/core/ext/xds/certificate_provider_store.h",
"src/core/ext/xds/file_watcher_certificate_provider_factory.cc",
"src/core/ext/xds/file_watcher_certificate_provider_factory.h",
"src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc",
"src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h",
"src/core/ext/xds/xds_api.cc",

@ -823,6 +823,7 @@ if(gRPC_BUILD_TESTS)
add_dependencies(buildtests_cxx examine_stack_test)
endif()
add_dependencies(buildtests_cxx exception_test)
add_dependencies(buildtests_cxx file_watcher_certificate_provider_factory_test)
add_dependencies(buildtests_cxx filter_end2end_test)
add_dependencies(buildtests_cxx flaky_network_test)
add_dependencies(buildtests_cxx generic_end2end_test)
@ -1708,6 +1709,7 @@ add_library(grpc
src/core/ext/upbdefs-generated/validate/validate.upbdefs.c
src/core/ext/xds/certificate_provider_registry.cc
src/core/ext/xds/certificate_provider_store.cc
src/core/ext/xds/file_watcher_certificate_provider_factory.cc
src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc
src/core/ext/xds/xds_api.cc
src/core/ext/xds/xds_bootstrap.cc
@ -11088,6 +11090,45 @@ target_link_libraries(exception_test
)
endif()
if(gRPC_BUILD_TESTS)
add_executable(file_watcher_certificate_provider_factory_test
test/core/xds/file_watcher_certificate_provider_factory_test.cc
third_party/googletest/googletest/src/gtest-all.cc
third_party/googletest/googlemock/src/gmock-all.cc
)
target_include_directories(file_watcher_certificate_provider_factory_test
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/include
${_gRPC_ADDRESS_SORTING_INCLUDE_DIR}
${_gRPC_RE2_INCLUDE_DIR}
${_gRPC_SSL_INCLUDE_DIR}
${_gRPC_UPB_GENERATED_DIR}
${_gRPC_UPB_GRPC_GENERATED_DIR}
${_gRPC_UPB_INCLUDE_DIR}
${_gRPC_ZLIB_INCLUDE_DIR}
third_party/googletest/googletest/include
third_party/googletest/googletest
third_party/googletest/googlemock/include
third_party/googletest/googlemock
${_gRPC_PROTO_GENS_DIR}
)
target_link_libraries(file_watcher_certificate_provider_factory_test
${_gRPC_PROTOBUF_LIBRARIES}
${_gRPC_ALLTARGETS_LIBRARIES}
grpc_test_util
grpc
gpr
address_sorting
upb
${_gRPC_GFLAGS_LIBRARIES}
)
endif()
if(gRPC_BUILD_TESTS)

@ -2109,6 +2109,7 @@ LIBGRPC_SRC = \
src/core/ext/upbdefs-generated/validate/validate.upbdefs.c \
src/core/ext/xds/certificate_provider_registry.cc \
src/core/ext/xds/certificate_provider_store.cc \
src/core/ext/xds/file_watcher_certificate_provider_factory.cc \
src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc \
src/core/ext/xds/xds_api.cc \
src/core/ext/xds/xds_bootstrap.cc \
@ -4776,6 +4777,7 @@ src/core/ext/upbdefs-generated/udpa/core/v1/resource_name.upbdefs.c: $(OPENSSL_D
src/core/ext/upbdefs-generated/validate/validate.upbdefs.c: $(OPENSSL_DEP)
src/core/ext/xds/certificate_provider_registry.cc: $(OPENSSL_DEP)
src/core/ext/xds/certificate_provider_store.cc: $(OPENSSL_DEP)
src/core/ext/xds/file_watcher_certificate_provider_factory.cc: $(OPENSSL_DEP)
src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc: $(OPENSSL_DEP)
src/core/ext/xds/xds_api.cc: $(OPENSSL_DEP)
src/core/ext/xds/xds_bootstrap.cc: $(OPENSSL_DEP)

@ -632,6 +632,7 @@ libs:
- src/core/ext/xds/certificate_provider_factory.h
- src/core/ext/xds/certificate_provider_registry.h
- src/core/ext/xds/certificate_provider_store.h
- src/core/ext/xds/file_watcher_certificate_provider_factory.h
- src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h
- src/core/ext/xds/xds_api.h
- src/core/ext/xds/xds_bootstrap.h
@ -1134,6 +1135,7 @@ libs:
- src/core/ext/upbdefs-generated/validate/validate.upbdefs.c
- src/core/ext/xds/certificate_provider_registry.cc
- src/core/ext/xds/certificate_provider_store.cc
- src/core/ext/xds/file_watcher_certificate_provider_factory.cc
- src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc
- src/core/ext/xds/xds_api.cc
- src/core/ext/xds/xds_bootstrap.cc
@ -5979,6 +5981,19 @@ targets:
- gpr
- address_sorting
- upb
- name: file_watcher_certificate_provider_factory_test
gtest: true
build: test
language: c++
headers: []
src:
- test/core/xds/file_watcher_certificate_provider_factory_test.cc
deps:
- grpc_test_util
- grpc
- gpr
- address_sorting
- upb
- name: filter_end2end_test
gtest: true
build: test

@ -312,6 +312,7 @@ if test "$PHP_GRPC" != "no"; then
src/core/ext/upbdefs-generated/validate/validate.upbdefs.c \
src/core/ext/xds/certificate_provider_registry.cc \
src/core/ext/xds/certificate_provider_store.cc \
src/core/ext/xds/file_watcher_certificate_provider_factory.cc \
src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc \
src/core/ext/xds/xds_api.cc \
src/core/ext/xds/xds_bootstrap.cc \

@ -279,6 +279,7 @@ if (PHP_GRPC != "no") {
"src\\core\\ext\\upbdefs-generated\\validate\\validate.upbdefs.c " +
"src\\core\\ext\\xds\\certificate_provider_registry.cc " +
"src\\core\\ext\\xds\\certificate_provider_store.cc " +
"src\\core\\ext\\xds\\file_watcher_certificate_provider_factory.cc " +
"src\\core\\ext\\xds\\google_mesh_ca_certificate_provider_factory.cc " +
"src\\core\\ext\\xds\\xds_api.cc " +
"src\\core\\ext\\xds\\xds_bootstrap.cc " +

@ -446,6 +446,7 @@ Pod::Spec.new do |s|
'src/core/ext/xds/certificate_provider_factory.h',
'src/core/ext/xds/certificate_provider_registry.h',
'src/core/ext/xds/certificate_provider_store.h',
'src/core/ext/xds/file_watcher_certificate_provider_factory.h',
'src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h',
'src/core/ext/xds/xds_api.h',
'src/core/ext/xds/xds_bootstrap.h',
@ -1054,6 +1055,7 @@ Pod::Spec.new do |s|
'src/core/ext/xds/certificate_provider_factory.h',
'src/core/ext/xds/certificate_provider_registry.h',
'src/core/ext/xds/certificate_provider_store.h',
'src/core/ext/xds/file_watcher_certificate_provider_factory.h',
'src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h',
'src/core/ext/xds/xds_api.h',
'src/core/ext/xds/xds_bootstrap.h',

@ -709,6 +709,8 @@ Pod::Spec.new do |s|
'src/core/ext/xds/certificate_provider_registry.h',
'src/core/ext/xds/certificate_provider_store.cc',
'src/core/ext/xds/certificate_provider_store.h',
'src/core/ext/xds/file_watcher_certificate_provider_factory.cc',
'src/core/ext/xds/file_watcher_certificate_provider_factory.h',
'src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc',
'src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h',
'src/core/ext/xds/xds_api.cc',
@ -1583,6 +1585,7 @@ Pod::Spec.new do |s|
'src/core/ext/xds/certificate_provider_factory.h',
'src/core/ext/xds/certificate_provider_registry.h',
'src/core/ext/xds/certificate_provider_store.h',
'src/core/ext/xds/file_watcher_certificate_provider_factory.h',
'src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h',
'src/core/ext/xds/xds_api.h',
'src/core/ext/xds/xds_bootstrap.h',

@ -626,6 +626,8 @@ Gem::Specification.new do |s|
s.files += %w( src/core/ext/xds/certificate_provider_registry.h )
s.files += %w( src/core/ext/xds/certificate_provider_store.cc )
s.files += %w( src/core/ext/xds/certificate_provider_store.h )
s.files += %w( src/core/ext/xds/file_watcher_certificate_provider_factory.cc )
s.files += %w( src/core/ext/xds/file_watcher_certificate_provider_factory.h )
s.files += %w( src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc )
s.files += %w( src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h )
s.files += %w( src/core/ext/xds/xds_api.cc )

@ -722,6 +722,7 @@
'src/core/ext/upbdefs-generated/validate/validate.upbdefs.c',
'src/core/ext/xds/certificate_provider_registry.cc',
'src/core/ext/xds/certificate_provider_store.cc',
'src/core/ext/xds/file_watcher_certificate_provider_factory.cc',
'src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc',
'src/core/ext/xds/xds_api.cc',
'src/core/ext/xds/xds_bootstrap.cc',

@ -606,6 +606,8 @@
<file baseinstalldir="/" name="src/core/ext/xds/certificate_provider_registry.h" role="src" />
<file baseinstalldir="/" name="src/core/ext/xds/certificate_provider_store.cc" role="src" />
<file baseinstalldir="/" name="src/core/ext/xds/certificate_provider_store.h" role="src" />
<file baseinstalldir="/" name="src/core/ext/xds/file_watcher_certificate_provider_factory.cc" role="src" />
<file baseinstalldir="/" name="src/core/ext/xds/file_watcher_certificate_provider_factory.h" role="src" />
<file baseinstalldir="/" name="src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc" role="src" />
<file baseinstalldir="/" name="src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h" role="src" />
<file baseinstalldir="/" name="src/core/ext/xds/xds_api.cc" role="src" />

@ -95,26 +95,19 @@ std::unique_ptr<ClientChannelMethodParsedConfig::RetryPolicy> ParseRetryPolicy(
}
}
// Parse initialBackoff.
it = json.object_value().find("initialBackoff");
if (it != json.object_value().end()) {
if (!ParseDurationFromJson(it->second, &retry_policy->initial_backoff)) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:initialBackoff error:Failed to parse"));
} else if (retry_policy->initial_backoff == 0) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:initialBackoff error:must be greater than 0"));
}
if (ParseJsonObjectFieldAsDuration(json.object_value(), "initialBackoff",
&retry_policy->initial_backoff,
&error_list) &&
retry_policy->initial_backoff == 0) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:initialBackoff error:must be greater than 0"));
}
// Parse maxBackoff.
it = json.object_value().find("maxBackoff");
if (it != json.object_value().end()) {
if (!ParseDurationFromJson(it->second, &retry_policy->max_backoff)) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:maxBackoff error:failed to parse"));
} else if (retry_policy->max_backoff == 0) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:maxBackoff error:should be greater than 0"));
}
if (ParseJsonObjectFieldAsDuration(json.object_value(), "maxBackoff",
&retry_policy->max_backoff, &error_list) &&
retry_policy->max_backoff == 0) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:maxBackoff error:should be greater than 0"));
}
// Parse backoffMultiplier.
it = json.object_value().find("backoffMultiplier");
@ -383,13 +376,8 @@ ClientChannelServiceConfigParser::ParsePerMethodParams(
}
}
// Parse timeout.
it = json.object_value().find("timeout");
if (it != json.object_value().end()) {
if (!ParseDurationFromJson(it->second, &timeout)) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:timeout error:Failed parsing"));
};
}
ParseJsonObjectFieldAsDuration(json.object_value(), "timeout", &timeout,
&error_list, false);
// Parse retry policy.
it = json.object_value().find("retryPolicy");
if (it != json.object_value().end()) {

@ -0,0 +1,119 @@
//
//
// Copyright 2020 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
#include <grpc/support/port_platform.h>
#include "src/core/ext/xds/file_watcher_certificate_provider_factory.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "src/core/lib/json/json_util.h"
namespace grpc_core {
namespace {
const char* kFileWatcherPlugin = "file_watcher";
} // namespace
//
// FileWatcherCertificateProviderFactory::Config
//
const char* FileWatcherCertificateProviderFactory::Config::name() const {
return kFileWatcherPlugin;
}
std::string FileWatcherCertificateProviderFactory::Config::ToString() const {
std::vector<std::string> parts;
parts.push_back("{");
if (!identity_cert_file_.empty()) {
parts.push_back(
absl::StrFormat("certificate_file=\"%s\", ", identity_cert_file_));
}
if (!identity_cert_file_.empty()) {
parts.push_back(
absl::StrFormat("private_key_file=\"%s\", ", private_key_file_));
}
if (!identity_cert_file_.empty()) {
parts.push_back(
absl::StrFormat("ca_certificate_file=\"%s\", ", root_cert_file_));
}
parts.push_back(
absl::StrFormat("refresh_interval=%ldms}", refresh_interval_ms_));
return absl::StrJoin(parts, "");
}
RefCountedPtr<FileWatcherCertificateProviderFactory::Config>
FileWatcherCertificateProviderFactory::Config::Parse(const Json& config_json,
grpc_error** error) {
auto config = MakeRefCounted<FileWatcherCertificateProviderFactory::Config>();
if (config_json.type() != Json::Type::OBJECT) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"error:config type should be OBJECT.");
return nullptr;
}
std::vector<grpc_error*> error_list;
ParseJsonObjectField(config_json.object_value(), "certificate_file",
&config->identity_cert_file_, &error_list, false);
ParseJsonObjectField(config_json.object_value(), "private_key_file",
&config->private_key_file_, &error_list, false);
if (config->identity_cert_file_.empty() !=
config->private_key_file_.empty()) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"fields \"certificate_file\" and \"private_key_file\" must be both set "
"or both unset."));
}
ParseJsonObjectField(config_json.object_value(), "ca_certificate_file",
&config->root_cert_file_, &error_list, false);
if (config->identity_cert_file_.empty() && config->root_cert_file_.empty()) {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"At least one of \"certificate_file\" and \"ca_certificate_file\" must "
"be specified."));
}
if (!ParseJsonObjectFieldAsDuration(
config_json.object_value(), "refresh_interval",
&config->refresh_interval_ms_, &error_list, false)) {
config->refresh_interval_ms_ = 10 * 60 * 1000; // 10 minutes default
}
if (!error_list.empty()) {
*error = GRPC_ERROR_CREATE_FROM_VECTOR(
"Error parsing file watcher certificate provider config", &error_list);
return nullptr;
}
return config;
}
//
// FileWatcherCertificateProviderFactory
//
const char* FileWatcherCertificateProviderFactory::name() const {
return kFileWatcherPlugin;
}
RefCountedPtr<CertificateProviderFactory::Config>
FileWatcherCertificateProviderFactory::CreateCertificateProviderConfig(
const Json& config_json, grpc_error** error) {
return FileWatcherCertificateProviderFactory::Config::Parse(config_json,
error);
}
} // namespace grpc_core

@ -0,0 +1,72 @@
//
//
// Copyright 2020 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
#ifndef GRPC_CORE_EXT_XDS_FILE_WATCHER_CERTIFICATE_PROVIDER_FACTORY_H
#define GRPC_CORE_EXT_XDS_FILE_WATCHER_CERTIFICATE_PROVIDER_FACTORY_H
#include <grpc/support/port_platform.h>
#include "src/core/ext/xds/certificate_provider_factory.h"
namespace grpc_core {
class FileWatcherCertificateProviderFactory
: public CertificateProviderFactory {
public:
class Config : public CertificateProviderFactory::Config {
public:
static RefCountedPtr<Config> Parse(const Json& config_json,
grpc_error** error);
const char* name() const override;
std::string ToString() const override;
const std::string& identity_cert_file() const {
return identity_cert_file_;
}
const std::string& private_key_file() const { return private_key_file_; }
const std::string& root_cert_file() const { return root_cert_file_; }
grpc_millis refresh_interval_ms() const { return refresh_interval_ms_; }
private:
std::string identity_cert_file_;
std::string private_key_file_;
std::string root_cert_file_;
grpc_millis refresh_interval_ms_;
};
const char* name() const override;
RefCountedPtr<CertificateProviderFactory::Config>
CreateCertificateProviderConfig(const Json& config_json,
grpc_error** error) override;
RefCountedPtr<grpc_tls_certificate_provider> CreateCertificateProvider(
RefCountedPtr<CertificateProviderFactory::Config> config) override {
// TODO(yashykt) : To be implemented
return nullptr;
}
};
} // namespace grpc_core
#endif // GRPC_CORE_EXT_XDS_FILE_WATCHER_CERTIFICATE_PROVIDER_FACTORY_H

@ -37,123 +37,6 @@ namespace {
const char* kMeshCaPlugin = "meshCA";
//
// Helper functions for extracting types from JSON
//
template <typename NumericType, typename ErrorVectorType>
bool ExtractJsonType(const Json& json, const std::string& field_name,
NumericType* output, ErrorVectorType* error_list) {
static_assert(std::is_integral<NumericType>::value, "Integral required");
if (json.type() != Json::Type::NUMBER) {
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be NUMBER")
.c_str()));
return false;
}
std::istringstream ss(json.string_value());
ss >> *output;
// The JSON parsing API should have dealt with parsing errors, but check
// anyway
if (GPR_UNLIKELY(ss.bad())) {
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:failed to parse.").c_str()));
return false;
}
return true;
}
template <typename ErrorVectorType>
bool ExtractJsonType(const Json& json, const std::string& field_name,
bool* output, ErrorVectorType* error_list) {
switch (json.type()) {
case Json::Type::JSON_TRUE:
*output = true;
return true;
case Json::Type::JSON_FALSE:
*output = false;
return true;
default:
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be BOOLEAN")
.c_str()));
return false;
}
}
template <typename ErrorVectorType>
bool ExtractJsonType(const Json& json, const std::string& field_name,
std::string* output, ErrorVectorType* error_list) {
if (json.type() != Json::Type::STRING) {
*output = "";
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be STRING")
.c_str()));
return false;
}
*output = json.string_value();
return true;
}
template <typename ErrorVectorType>
bool ExtractJsonType(const Json& json, const std::string& field_name,
const Json::Array** output, ErrorVectorType* error_list) {
if (json.type() != Json::Type::ARRAY) {
*output = nullptr;
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be ARRAY")
.c_str()));
return false;
}
*output = &json.array_value();
return true;
}
template <typename ErrorVectorType>
bool ExtractJsonType(const Json& json, const std::string& field_name,
const Json::Object** output, ErrorVectorType* error_list) {
if (json.type() != Json::Type::OBJECT) {
*output = nullptr;
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be OBJECT")
.c_str()));
return false;
}
*output = &json.object_value();
return true;
}
template <typename ErrorVectorType>
bool ExtractJsonType(const Json& json, const std::string& field_name,
grpc_millis* output, ErrorVectorType* error_list) {
if (!ParseDurationFromJson(json, output)) {
*output = GRPC_MILLIS_INF_PAST;
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name,
" error:type should be STRING of the form given by "
"google.proto.Duration.")
.c_str()));
return false;
}
return true;
}
template <typename T, typename ErrorVectorType>
bool ParseJsonObjectField(const Json::Object& object,
const std::string& field_name, T* output,
ErrorVectorType* error_list, bool optional = false) {
auto it = object.find(field_name);
if (it == object.end()) {
if (!optional) {
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:does not exist.")
.c_str()));
}
return false;
}
auto& child_object_json = it->second;
return ExtractJsonType(child_object_json, field_name, output, error_list);
}
} // namespace
//
@ -175,22 +58,22 @@ GoogleMeshCaCertificateProviderFactory::Config::ParseJsonObjectStsService(
std::vector<grpc_error*> error_list_sts_service;
if (!ParseJsonObjectField(sts_service, "token_exchange_service_uri",
&sts_config_.token_exchange_service_uri,
&error_list_sts_service, true)) {
&error_list_sts_service, false)) {
sts_config_.token_exchange_service_uri =
"securetoken.googleapis.com"; // default
}
ParseJsonObjectField(sts_service, "resource", &sts_config_.resource,
&error_list_sts_service, true);
&error_list_sts_service, false);
ParseJsonObjectField(sts_service, "audience", &sts_config_.audience,
&error_list_sts_service, true);
&error_list_sts_service, false);
if (!ParseJsonObjectField(sts_service, "scope", &sts_config_.scope,
&error_list_sts_service, true)) {
&error_list_sts_service, false)) {
sts_config_.scope =
"https://www.googleapis.com/auth/cloud-platform"; // default
}
ParseJsonObjectField(sts_service, "requested_token_type",
&sts_config_.requested_token_type,
&error_list_sts_service, true);
&error_list_sts_service, false);
ParseJsonObjectField(sts_service, "subject_token_path",
&sts_config_.subject_token_path,
&error_list_sts_service);
@ -199,10 +82,10 @@ GoogleMeshCaCertificateProviderFactory::Config::ParseJsonObjectStsService(
&error_list_sts_service);
ParseJsonObjectField(sts_service, "actor_token_path",
&sts_config_.actor_token_path, &error_list_sts_service,
true);
false);
ParseJsonObjectField(sts_service, "actor_token_type",
&sts_config_.actor_token_type, &error_list_sts_service,
true);
false);
return error_list_sts_service;
}
@ -228,7 +111,7 @@ GoogleMeshCaCertificateProviderFactory::Config::ParseJsonObjectGoogleGrpc(
const Json::Object& google_grpc) {
std::vector<grpc_error*> error_list_google_grpc;
if (!ParseJsonObjectField(google_grpc, "target_uri", &endpoint_,
&error_list_google_grpc, true)) {
&error_list_google_grpc, false)) {
endpoint_ = "meshca.googleapis.com"; // Default target
}
const Json::Array* call_credentials_array = nullptr;
@ -268,8 +151,8 @@ GoogleMeshCaCertificateProviderFactory::Config::ParseJsonObjectGrpcServices(
"field:google_grpc", &error_list_google_grpc));
}
}
if (!ParseJsonObjectField(grpc_service, "timeout", &timeout_,
&error_list_grpc_services, true)) {
if (!ParseJsonObjectFieldAsDuration(grpc_service, "timeout", &timeout_,
&error_list_grpc_services, false)) {
timeout_ = 10 * 1000; // 10sec default
}
return error_list_grpc_services;
@ -281,7 +164,7 @@ GoogleMeshCaCertificateProviderFactory::Config::ParseJsonObjectServer(
std::vector<grpc_error*> error_list_server;
std::string api_type;
if (ParseJsonObjectField(server, "api_type", &api_type, &error_list_server,
true)) {
false)) {
if (api_type != "GRPC") {
error_list_server.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:api_type error:Only GRPC is supported"));
@ -330,30 +213,30 @@ GoogleMeshCaCertificateProviderFactory::Config::Parse(const Json& config_json,
GRPC_ERROR_CREATE_FROM_VECTOR("field:server", &error_list_server));
}
}
if (!ParseJsonObjectField(config_json.object_value(), "certificate_lifetime",
&config->certificate_lifetime_, &error_list,
true)) {
if (!ParseJsonObjectFieldAsDuration(
config_json.object_value(), "certificate_lifetime",
&config->certificate_lifetime_, &error_list, false)) {
config->certificate_lifetime_ = 24 * 60 * 60 * 1000; // 24hrs default
}
if (!ParseJsonObjectField(config_json.object_value(), "renewal_grace_period",
&config->renewal_grace_period_, &error_list,
true)) {
if (!ParseJsonObjectFieldAsDuration(
config_json.object_value(), "renewal_grace_period",
&config->renewal_grace_period_, &error_list, false)) {
config->renewal_grace_period_ = 12 * 60 * 60 * 1000; // 12hrs default
}
std::string key_type;
if (ParseJsonObjectField(config_json.object_value(), "key_type", &key_type,
&error_list, true)) {
&error_list, false)) {
if (key_type != "RSA") {
error_list.push_back(GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"field:key_type error:Only RSA is supported."));
}
}
if (!ParseJsonObjectField(config_json.object_value(), "key_size",
&config->key_size_, &error_list, true)) {
&config->key_size_, &error_list, false)) {
config->key_size_ = 2048; // default 2048 bit key size
}
if (!ParseJsonObjectField(config_json.object_value(), "location",
&config->location_, &error_list, true)) {
&config->location_, &error_list, false)) {
// GCE/GKE Metadata server needs to be contacted to get the value.
}
if (!error_list.empty()) {

@ -21,6 +21,9 @@
#include <grpc/support/port_platform.h>
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "src/core/lib/iomgr/exec_ctx.h"
#include "src/core/lib/json/json.h"
@ -32,6 +35,170 @@ namespace grpc_core {
// Returns true on success, false otherwise.
bool ParseDurationFromJson(const Json& field, grpc_millis* duration);
//
// Helper functions for extracting types from JSON.
// Return true on success, false otherwise. If an error is encountered during
// parsing, a descriptive error is appended to \a error_list.
//
template <typename NumericType, typename ErrorVectorType>
inline bool ExtractJsonNumber(const Json& json, const std::string& field_name,
NumericType* output,
ErrorVectorType* error_list) {
static_assert(std::is_integral<NumericType>::value, "Integral required");
if (json.type() != Json::Type::NUMBER) {
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be NUMBER")
.c_str()));
return false;
}
if (!absl::SimpleAtoi(json.string_value(), output)) {
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:failed to parse.").c_str()));
return false;
}
return true;
}
template <typename ErrorVectorType>
inline bool ExtractJsonBool(const Json& json, const std::string& field_name,
bool* output, ErrorVectorType* error_list) {
switch (json.type()) {
case Json::Type::JSON_TRUE:
*output = true;
return true;
case Json::Type::JSON_FALSE:
*output = false;
return true;
default:
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be BOOLEAN")
.c_str()));
return false;
}
}
template <typename ErrorVectorType>
inline bool ExtractJsonString(const Json& json, const std::string& field_name,
std::string* output,
ErrorVectorType* error_list) {
if (json.type() != Json::Type::STRING) {
*output = "";
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be STRING")
.c_str()));
return false;
}
*output = json.string_value();
return true;
}
template <typename ErrorVectorType>
inline bool ExtractJsonArray(const Json& json, const std::string& field_name,
const Json::Array** output,
ErrorVectorType* error_list) {
if (json.type() != Json::Type::ARRAY) {
*output = nullptr;
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be ARRAY")
.c_str()));
return false;
}
*output = &json.array_value();
return true;
}
template <typename ErrorVectorType>
inline bool ExtractJsonObject(const Json& json, const std::string& field_name,
const Json::Object** output,
ErrorVectorType* error_list) {
if (json.type() != Json::Type::OBJECT) {
*output = nullptr;
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:type should be OBJECT")
.c_str()));
return false;
}
*output = &json.object_value();
return true;
}
template <typename NumericType, typename ErrorVectorType>
inline bool ExtractJsonType(const Json& json, const std::string& field_name,
NumericType* output, ErrorVectorType* error_list) {
return ExtractJsonNumber(json, field_name, output, error_list);
}
template <typename ErrorVectorType>
inline bool ExtractJsonType(const Json& json, const std::string& field_name,
bool* output, ErrorVectorType* error_list) {
return ExtractJsonBool(json, field_name, output, error_list);
}
template <typename ErrorVectorType>
inline bool ExtractJsonType(const Json& json, const std::string& field_name,
std::string* output, ErrorVectorType* error_list) {
return ExtractJsonString(json, field_name, output, error_list);
}
template <typename ErrorVectorType>
inline bool ExtractJsonType(const Json& json, const std::string& field_name,
const Json::Array** output,
ErrorVectorType* error_list) {
return ExtractJsonArray(json, field_name, output, error_list);
}
template <typename ErrorVectorType>
inline bool ExtractJsonType(const Json& json, const std::string& field_name,
const Json::Object** output,
ErrorVectorType* error_list) {
return ExtractJsonObject(json, field_name, output, error_list);
}
template <typename T, typename ErrorVectorType>
inline bool ParseJsonObjectField(const Json::Object& object,
const std::string& field_name, T* output,
ErrorVectorType* error_list,
bool required = true) {
auto it = object.find(field_name);
if (it == object.end()) {
if (required) {
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:does not exist.")
.c_str()));
}
return false;
}
auto& child_object_json = it->second;
return ExtractJsonType(child_object_json, field_name, output, error_list);
}
template <typename ErrorVectorType>
inline bool ParseJsonObjectFieldAsDuration(const Json::Object& object,
const std::string& field_name,
grpc_millis* output,
ErrorVectorType* error_list,
bool required = true) {
auto it = object.find(field_name);
if (it == object.end()) {
if (required) {
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name, " error:does not exist.")
.c_str()));
}
return false;
}
if (!ParseDurationFromJson(it->second, output)) {
*output = GRPC_MILLIS_INF_PAST;
error_list->push_back(GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrCat("field:", field_name,
" error:type should be STRING of the form given by "
"google.proto.Duration.")
.c_str()));
return false;
}
return true;
}
} // namespace grpc_core
#endif // GRPC_CORE_LIB_JSON_JSON_UTIL_H

@ -288,6 +288,7 @@ CORE_SOURCE_FILES = [
'src/core/ext/upbdefs-generated/validate/validate.upbdefs.c',
'src/core/ext/xds/certificate_provider_registry.cc',
'src/core/ext/xds/certificate_provider_store.cc',
'src/core/ext/xds/file_watcher_certificate_provider_factory.cc',
'src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc',
'src/core/ext/xds/xds_api.cc',
'src/core/ext/xds/xds_bootstrap.cc',

@ -733,7 +733,8 @@ TEST_F(ClientChannelParserTest, InvalidTimeout) {
"Method Params.*referenced_errors.*"
"methodConfig.*referenced_errors.*"
"Client channel parser.*referenced_errors.*"
"field:timeout error:Failed parsing"));
"field:timeout error:type should be STRING of the form given "
"by google.proto.Duration"));
GRPC_ERROR_UNREF(error);
}
@ -876,7 +877,8 @@ TEST_F(ClientChannelParserTest, InvalidRetryPolicyInitialBackoff) {
"methodConfig.*referenced_errors.*"
"Client channel parser.*referenced_errors.*"
"retryPolicy.*referenced_errors.*"
"field:initialBackoff error:Failed to parse"));
"field:initialBackoff error:type should be STRING of the "
"form given by google.proto.Duration"));
GRPC_ERROR_UNREF(error);
}
@ -905,7 +907,8 @@ TEST_F(ClientChannelParserTest, InvalidRetryPolicyMaxBackoff) {
"methodConfig.*referenced_errors.*"
"Client channel parser.*referenced_errors.*"
"retryPolicy.*referenced_errors.*"
"field:maxBackoff error:failed to parse"));
"field:maxBackoff error:type should be STRING of the form "
"given by google.proto.Duration"));
GRPC_ERROR_UNREF(error);
}

@ -44,6 +44,18 @@ grpc_cc_test(
],
)
grpc_cc_test(
name = "file_watcher_certificate_provider_factory_test",
srcs = ["file_watcher_certificate_provider_factory_test.cc"],
external_deps = ["gtest"],
language = "C++",
deps = [
"//:gpr",
"//:grpc",
"//test/core/util:grpc_test_util",
],
)
grpc_cc_test(
name = "google_mesh_ca_certificate_provider_factory_test",
srcs = ["google_mesh_ca_certificate_provider_factory_test.cc"],

@ -0,0 +1,202 @@
//
//
// Copyright 2020 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
#include "src/core/ext/xds/file_watcher_certificate_provider_factory.h"
#include "absl/strings/str_format.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <grpc/grpc.h>
#include "test/core/util/test_config.h"
namespace grpc_core {
namespace testing {
namespace {
const char* kIdentityCertFile = "/path/to/identity_cert_file";
const char* kPrivateKeyFile = "/path/to/private_key_file";
const char* kRootCertFile = "/path/to/root_cert_file";
const int kRefreshInterval = 400;
TEST(FileWatcherConfigTest, Basic) {
std::string json_str = absl::StrFormat(
"{"
" \"certificate_file\": \"%s\","
" \"private_key_file\": \"%s\","
" \"ca_certificate_file\": \"%s\","
" \"refresh_interval\": \"%ds\""
"}",
kIdentityCertFile, kPrivateKeyFile, kRootCertFile, kRefreshInterval);
grpc_error* error = GRPC_ERROR_NONE;
Json json = Json::Parse(json_str, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
auto config =
FileWatcherCertificateProviderFactory::Config::Parse(json, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
EXPECT_EQ(config->identity_cert_file(), kIdentityCertFile);
EXPECT_EQ(config->private_key_file(), kPrivateKeyFile);
EXPECT_EQ(config->root_cert_file(), kRootCertFile);
EXPECT_EQ(config->refresh_interval_ms(), kRefreshInterval * 1000);
}
TEST(FileWatcherConfigTest, DefaultRefreshInterval) {
std::string json_str = absl::StrFormat(
"{"
" \"certificate_file\": \"%s\","
" \"private_key_file\": \"%s\","
" \"ca_certificate_file\": \"%s\""
"}",
kIdentityCertFile, kPrivateKeyFile, kRootCertFile);
grpc_error* error = GRPC_ERROR_NONE;
Json json = Json::Parse(json_str, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
auto config =
FileWatcherCertificateProviderFactory::Config::Parse(json, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
EXPECT_EQ(config->identity_cert_file(), kIdentityCertFile);
EXPECT_EQ(config->private_key_file(), kPrivateKeyFile);
EXPECT_EQ(config->root_cert_file(), kRootCertFile);
EXPECT_EQ(config->refresh_interval_ms(), 600 * 1000);
}
TEST(FileWatcherConfigTest, OnlyRootCertificatesFileProvided) {
std::string json_str = absl::StrFormat(
"{"
" \"ca_certificate_file\": \"%s\""
"}",
kRootCertFile);
grpc_error* error = GRPC_ERROR_NONE;
Json json = Json::Parse(json_str, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
auto config =
FileWatcherCertificateProviderFactory::Config::Parse(json, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
EXPECT_TRUE(config->identity_cert_file().empty());
EXPECT_TRUE(config->private_key_file().empty());
EXPECT_EQ(config->root_cert_file(), kRootCertFile);
EXPECT_EQ(config->refresh_interval_ms(), 600 * 1000);
}
TEST(FileWatcherConfigTest, OnlyIdenityCertificatesAndPrivateKeyProvided) {
std::string json_str = absl::StrFormat(
"{"
" \"certificate_file\": \"%s\","
" \"private_key_file\": \"%s\""
"}",
kIdentityCertFile, kPrivateKeyFile);
grpc_error* error = GRPC_ERROR_NONE;
Json json = Json::Parse(json_str, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
auto config =
FileWatcherCertificateProviderFactory::Config::Parse(json, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
EXPECT_EQ(config->identity_cert_file(), kIdentityCertFile);
EXPECT_EQ(config->private_key_file(), kPrivateKeyFile);
EXPECT_TRUE(config->root_cert_file().empty());
EXPECT_EQ(config->refresh_interval_ms(), 600 * 1000);
}
TEST(FileWatcherConfigTest, WrongTypes) {
const char* json_str =
"{"
" \"certificate_file\": 123,"
" \"private_key_file\": 123,"
" \"ca_certificate_file\": 123,"
" \"refresh_interval\": 123"
"}";
grpc_error* error = GRPC_ERROR_NONE;
Json json = Json::Parse(json_str, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
auto config =
FileWatcherCertificateProviderFactory::Config::Parse(json, &error);
EXPECT_THAT(grpc_error_string(error),
::testing::ContainsRegex(
"field:certificate_file error:type should be STRING.*"
"field:private_key_file error:type should be STRING.*"
"field:ca_certificate_file error:type should be STRING.*"
"field:refresh_interval error:type should be STRING of the "
"form given by "
"google.proto.Duration.*"));
GRPC_ERROR_UNREF(error);
}
TEST(FileWatcherConfigTest, IdentityCertProvidedButPrivateKeyMissing) {
std::string json_str = absl::StrFormat(
"{"
" \"certificate_file\": \"%s\""
"}",
kIdentityCertFile);
grpc_error* error = GRPC_ERROR_NONE;
Json json = Json::Parse(json_str, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
auto config =
FileWatcherCertificateProviderFactory::Config::Parse(json, &error);
EXPECT_THAT(grpc_error_string(error),
::testing::ContainsRegex(
"fields \"certificate_file\" and \"private_key_file\" must "
"be both set or both unset."));
GRPC_ERROR_UNREF(error);
}
TEST(FileWatcherConfigTest, PrivateKeyProvidedButIdentityCertMissing) {
std::string json_str = absl::StrFormat(
"{"
" \"private_key_file\": \"%s\""
"}",
kPrivateKeyFile);
grpc_error* error = GRPC_ERROR_NONE;
Json json = Json::Parse(json_str, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
auto config =
FileWatcherCertificateProviderFactory::Config::Parse(json, &error);
EXPECT_THAT(grpc_error_string(error),
::testing::ContainsRegex(
"fields \"certificate_file\" and \"private_key_file\" must "
"be both set or both unset."));
GRPC_ERROR_UNREF(error);
}
TEST(FileWatcherConfigTest, EmptyJsonObject) {
std::string json_str = absl::StrFormat("{}");
grpc_error* error = GRPC_ERROR_NONE;
Json json = Json::Parse(json_str, &error);
ASSERT_EQ(error, GRPC_ERROR_NONE) << grpc_error_string(error);
auto config =
FileWatcherCertificateProviderFactory::Config::Parse(json, &error);
EXPECT_THAT(
grpc_error_string(error),
::testing::ContainsRegex("At least one of \"certificate_file\" and "
"\"ca_certificate_file\" must be specified."));
GRPC_ERROR_UNREF(error);
}
} // namespace
} // namespace testing
} // namespace grpc_core
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
grpc::testing::TestEnvironment env(argc, argv);
grpc_init();
auto result = RUN_ALL_TESTS();
grpc_shutdown();
return result;
}

@ -1558,6 +1558,8 @@ src/core/ext/xds/certificate_provider_registry.cc \
src/core/ext/xds/certificate_provider_registry.h \
src/core/ext/xds/certificate_provider_store.cc \
src/core/ext/xds/certificate_provider_store.h \
src/core/ext/xds/file_watcher_certificate_provider_factory.cc \
src/core/ext/xds/file_watcher_certificate_provider_factory.h \
src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc \
src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h \
src/core/ext/xds/xds_api.cc \

@ -1395,6 +1395,8 @@ src/core/ext/xds/certificate_provider_registry.cc \
src/core/ext/xds/certificate_provider_registry.h \
src/core/ext/xds/certificate_provider_store.cc \
src/core/ext/xds/certificate_provider_store.h \
src/core/ext/xds/file_watcher_certificate_provider_factory.cc \
src/core/ext/xds/file_watcher_certificate_provider_factory.h \
src/core/ext/xds/google_mesh_ca_certificate_provider_factory.cc \
src/core/ext/xds/google_mesh_ca_certificate_provider_factory.h \
src/core/ext/xds/xds_api.cc \

@ -4455,6 +4455,30 @@
],
"uses_polling": true
},
{
"args": [],
"benchmark": false,
"ci_platforms": [
"linux",
"mac",
"posix",
"windows"
],
"cpu_cost": 1.0,
"exclude_configs": [],
"exclude_iomgrs": [],
"flaky": false,
"gtest": true,
"language": "c++",
"name": "file_watcher_certificate_provider_factory_test",
"platforms": [
"linux",
"mac",
"posix",
"windows"
],
"uses_polling": true
},
{
"args": [],
"benchmark": false,

Loading…
Cancel
Save