From 7c97bc7c275c2b768b48d727eae0db2dea5405bb Mon Sep 17 00:00:00 2001 From: Nikita Poltorapavlo Date: Tue, 5 Mar 2024 17:52:57 +0200 Subject: [PATCH] RDK-47994 : Secure Storage Thunder Plugin Reason for change: Account scope. Tests written for all the test levels (L0,L1,L2) for the entire plugin. Test Procedure: None Risks: None Signed-off-by: Nikita Poltorapavlo --- .github/workflows/L0-PersistentStore-grpc.yml | 53 +++ .github/workflows/L2-PersistentStore-grpc.yml | 29 ++ PersistentStore/CMakeLists.txt | 54 ++- PersistentStore/Module.h | 1 + PersistentStore/PersistentStore.conf.in | 2 + PersistentStore/PersistentStore.config | 19 +- PersistentStore/PersistentStore.cpp | 1 + PersistentStore/PersistentStore.h | 2 + PersistentStore/grpc/Store2.cpp | 8 + PersistentStore/grpc/Store2.h | 329 +++++++++++++++ PersistentStore/grpc/l0test/CMakeLists.txt | 69 +++ .../grpc/l0test/SecureStorageServiceMock.h | 14 + PersistentStore/grpc/l0test/Server.h | 20 + PersistentStore/grpc/l0test/Store2Test.cpp | 168 ++++++++ PersistentStore/grpc/l2test/CMakeLists.txt | 64 +++ PersistentStore/grpc/l2test/StubTest.cpp | 396 ++++++++++++++++++ .../grpc/secure_storage/secure_storage.proto | 111 +++++ 17 files changed, 1321 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/L0-PersistentStore-grpc.yml create mode 100644 .github/workflows/L2-PersistentStore-grpc.yml create mode 100644 PersistentStore/grpc/Store2.cpp create mode 100644 PersistentStore/grpc/Store2.h create mode 100644 PersistentStore/grpc/l0test/CMakeLists.txt create mode 100644 PersistentStore/grpc/l0test/SecureStorageServiceMock.h create mode 100644 PersistentStore/grpc/l0test/Server.h create mode 100644 PersistentStore/grpc/l0test/Store2Test.cpp create mode 100644 PersistentStore/grpc/l2test/CMakeLists.txt create mode 100644 PersistentStore/grpc/l2test/StubTest.cpp create mode 100644 PersistentStore/grpc/secure_storage/secure_storage.proto diff --git a/.github/workflows/L0-PersistentStore-grpc.yml b/.github/workflows/L0-PersistentStore-grpc.yml new file mode 100644 index 0000000000..769021aa4e --- /dev/null +++ b/.github/workflows/L0-PersistentStore-grpc.yml @@ -0,0 +1,53 @@ +name: L0-PersistentStore-grpc + +on: + push: + paths: + - PersistentStore/grpc/** + pull_request: + paths: + - PersistentStore/grpc/** + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + path: ${{github.repository}} + + - name: Install valgrind, coverage, cmake, protoc, grpc_cpp_plugin, grpc + run: | + sudo apt update + sudo apt install -y valgrind lcov cmake protobuf-compiler protobuf-compiler-grpc libgrpc++-dev + + - name: Build Thunder + working-directory: ${{github.workspace}} + run: sh +x ${GITHUB_REPOSITORY}/.github/workflows/BuildThunder.sh + + - name: Build + working-directory: ${{github.workspace}} + run: | + cmake -S ${GITHUB_REPOSITORY}/PersistentStore/grpc/l0test -B build/grpcl0test -DCMAKE_INSTALL_PREFIX="install/usr" -DCMAKE_CXX_FLAGS="--coverage -Wall -Werror" + cmake --build build/grpcl0test --target install + + - name: Run + working-directory: ${{github.workspace}} + run: PATH=${PWD}/install/usr/bin:${PATH} LD_LIBRARY_PATH=${PWD}/install/usr/lib:${LD_LIBRARY_PATH} valgrind --tool=memcheck --log-file=valgrind_log --leak-check=yes --show-reachable=yes --track-fds=yes --fair-sched=try grpcl0test + + - name: Generate coverage + working-directory: ${{github.workspace}} + run: | + lcov -c -o coverage.info -d build/grpcl0test + genhtml -o coverage coverage.info + + - name: Upload artifacts + if: ${{ !env.ACT }} + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: | + coverage/ + valgrind_log + if-no-files-found: warn diff --git a/.github/workflows/L2-PersistentStore-grpc.yml b/.github/workflows/L2-PersistentStore-grpc.yml new file mode 100644 index 0000000000..4384d0fbc2 --- /dev/null +++ b/.github/workflows/L2-PersistentStore-grpc.yml @@ -0,0 +1,29 @@ +name: L2-PersistentStore-grpc + +on: + push: + paths: + - PersistentStore/grpc/** + pull_request: + paths: + - PersistentStore/grpc/** + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + path: ${{github.repository}} + + - name: Install cmake, protoc, grpc_cpp_plugin, grpc + run: | + sudo apt update + sudo apt install -y cmake protobuf-compiler protobuf-compiler-grpc libgrpc++-dev + + - name: Build + working-directory: ${{github.workspace}} + run: | + cmake -S ${GITHUB_REPOSITORY}/PersistentStore/grpc/l2test -B build/grpcl2test -DCMAKE_INSTALL_PREFIX="install/usr" -DCMAKE_CXX_FLAGS="-Wall -Werror" + cmake --build build/grpcl2test --target install diff --git a/PersistentStore/CMakeLists.txt b/PersistentStore/CMakeLists.txt index 1afd301c4d..0494d43a92 100644 --- a/PersistentStore/CMakeLists.txt +++ b/PersistentStore/CMakeLists.txt @@ -24,7 +24,7 @@ set(CMAKE_CXX_STANDARD 11) find_package(WPEFramework) set(MODULE_NAME "${NAMESPACE}${PROJECT_NAME}") -set(PLUGIN_PERSISTENTSTORE_MODE "Off" CACHE STRING "Controls if the plugin should run in its own process, in process or remote") +set(PLUGIN_PERSISTENTSTORE_MODE "Local" CACHE STRING "Controls if the plugin should run in its own process, in process or remote") set(PLUGIN_PERSISTENTSTORE_URI "ss.eu.prod.developer.comcast.com:443" CACHE STRING "Account scope endpoint") set(PLUGIN_PERSISTENTSTORE_PATH "/opt/secure/persistent/rdkservicestore" CACHE STRING "Path") set(PLUGIN_PERSISTENTSTORE_LEGACYPATH "/opt/persistent/rdkservicestore" CACHE STRING "Previously used path") @@ -32,39 +32,75 @@ set(PLUGIN_PERSISTENTSTORE_KEY "" CACHE STRING "Encryption key") set(PLUGIN_PERSISTENTSTORE_MAXSIZE "1000000" CACHE STRING "For all text data, in bytes") set(PLUGIN_PERSISTENTSTORE_MAXVALUE "3000" CACHE STRING "For single text data, in bytes") set(PLUGIN_PERSISTENTSTORE_LIMIT "10000" CACHE STRING "Default for all text data in namespace, in bytes") +set(PLUGIN_PERSISTENTSTORE_TOKEN_COMMAND "" CACHE STRING "Shell command to get the service access token") set(PLUGIN_PERSISTENTSTORE_STARTUPORDER "" CACHE STRING "To configure startup order of PersistentStore plugin") add_library(${MODULE_NAME} SHARED PersistentStore.cpp PersistentStoreJsonRpc.cpp + Module.cpp +) + +find_package(${NAMESPACE}Plugins REQUIRED) +find_package(${NAMESPACE}Definitions REQUIRED) +target_link_libraries(${MODULE_NAME} PRIVATE + ${NAMESPACE}Plugins::${NAMESPACE}Plugins + ${NAMESPACE}Definitions::${NAMESPACE}Definitions +) + +install(TARGETS ${MODULE_NAME} + DESTINATION lib/${STORAGE_DIRECTORY}/plugins) + +set(PLUGIN_IMPLEMENTATION ${MODULE_NAME}Implementation) +add_library(${PLUGIN_IMPLEMENTATION} SHARED sqlite/Store2.cpp sqlite/StoreCache.cpp sqlite/StoreInspector.cpp sqlite/StoreLimit.cpp + grpc/Store2.cpp Module.cpp ) -find_package(${NAMESPACE}Plugins REQUIRED) -find_package(${NAMESPACE}Definitions REQUIRED) -target_link_libraries(${MODULE_NAME} PRIVATE +target_link_libraries(${PLUGIN_IMPLEMENTATION} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins ${NAMESPACE}Definitions::${NAMESPACE}Definitions ) find_package(PkgConfig REQUIRED) pkg_search_module(SQLITE REQUIRED sqlite3) -target_link_libraries(${MODULE_NAME} PRIVATE ${SQLITE_LIBRARIES}) +target_link_libraries(${PLUGIN_IMPLEMENTATION} PRIVATE ${SQLITE_LIBRARIES}) find_library(IARMBUS_LIBRARIES NAMES IARMBus) if (IARMBUS_LIBRARIES) find_path(IARMBUS_INCLUDE_DIRS NAMES libIBus.h PATH_SUFFIXES rdk/iarmbus REQUIRED) find_path(IARMSYS_INCLUDE_DIRS NAMES sysMgr.h PATH_SUFFIXES rdk/iarmmgrs/sysmgr REQUIRED) - target_include_directories(${MODULE_NAME} PRIVATE ${IARMBUS_INCLUDE_DIRS} ${IARMSYS_INCLUDE_DIRS}) - target_link_libraries(${MODULE_NAME} PRIVATE ${IARMBUS_LIBRARIES}) - add_definitions(-DWITH_SYSMGR) + target_include_directories(${PLUGIN_IMPLEMENTATION} PRIVATE ${IARMBUS_INCLUDE_DIRS} ${IARMSYS_INCLUDE_DIRS}) + target_link_libraries(${PLUGIN_IMPLEMENTATION} PRIVATE ${IARMBUS_LIBRARIES}) + target_compile_definitions(${PLUGIN_IMPLEMENTATION} PRIVATE WITH_SYSMGR) endif () -install(TARGETS ${MODULE_NAME} +find_package(Protobuf REQUIRED) +target_link_libraries(${PLUGIN_IMPLEMENTATION} PRIVATE ${Protobuf_LIBRARIES}) + +add_custom_target(protoc + ${Protobuf_PROTOC_EXECUTABLE} --cpp_out ${CMAKE_CURRENT_BINARY_DIR} -I ${CMAKE_CURRENT_SOURCE_DIR}/grpc/secure_storage ${CMAKE_CURRENT_SOURCE_DIR}/grpc/secure_storage/secure_storage.proto +) +add_dependencies(${PLUGIN_IMPLEMENTATION} protoc) + +target_link_libraries(${PLUGIN_IMPLEMENTATION} PRIVATE grpc++) +find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) + +add_custom_target(protoc-gen-grpc + ${Protobuf_PROTOC_EXECUTABLE} --grpc_out ${CMAKE_CURRENT_BINARY_DIR} --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN} -I ${CMAKE_CURRENT_SOURCE_DIR}/grpc/secure_storage ${CMAKE_CURRENT_SOURCE_DIR}/grpc/secure_storage/secure_storage.proto +) +add_dependencies(${PLUGIN_IMPLEMENTATION} protoc-gen-grpc) + +set(PROTO_SRCS secure_storage.pb.cc secure_storage.grpc.pb.cc) +target_sources(${PLUGIN_IMPLEMENTATION} PRIVATE ${PROTO_SRCS}) +set_property(SOURCE ${PROTO_SRCS} PROPERTY GENERATED 1) +target_include_directories(${PLUGIN_IMPLEMENTATION} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +install(TARGETS ${PLUGIN_IMPLEMENTATION} DESTINATION lib/${STORAGE_DIRECTORY}/plugins) write_config() diff --git a/PersistentStore/Module.h b/PersistentStore/Module.h index 509556a238..8f47e9d58e 100644 --- a/PersistentStore/Module.h +++ b/PersistentStore/Module.h @@ -34,6 +34,7 @@ #define MAXSIZE_ENV "PERSISTENTSTORE_MAXSIZE" #define MAXVALUE_ENV "PERSISTENTSTORE_MAXVALUE" #define LIMIT_ENV "PERSISTENTSTORE_LIMIT" +#define TOKEN_COMMAND_ENV "PERSISTENTSTORE_TOKEN_COMMAND" #define IARM_INIT_NAME "Thunder_Plugins" #undef EXTERNAL diff --git a/PersistentStore/PersistentStore.conf.in b/PersistentStore/PersistentStore.conf.in index 3bdad23e1f..244967ed15 100644 --- a/PersistentStore/PersistentStore.conf.in +++ b/PersistentStore/PersistentStore.conf.in @@ -6,6 +6,7 @@ configuration = JSON() rootobject = JSON() rootobject.add("mode", "@PLUGIN_PERSISTENTSTORE_MODE@") +rootobject.add("locator", "lib@PLUGIN_IMPLEMENTATION@.so") configuration.add("root", rootobject) configuration.add("uri", "@PLUGIN_PERSISTENTSTORE_URI@") @@ -15,3 +16,4 @@ configuration.add("key", "@PLUGIN_PERSISTENTSTORE_KEY@") configuration.add("maxsize", "@PLUGIN_PERSISTENTSTORE_MAXSIZE@") configuration.add("maxvalue", "@PLUGIN_PERSISTENTSTORE_MAXVALUE@") configuration.add("limit", "@PLUGIN_PERSISTENTSTORE_LIMIT@") +configuration.add("tokencommand", "@PLUGIN_PERSISTENTSTORE_TOKEN_COMMAND@") diff --git a/PersistentStore/PersistentStore.config b/PersistentStore/PersistentStore.config index d76b03859e..5d6d4c59cd 100644 --- a/PersistentStore/PersistentStore.config +++ b/PersistentStore/PersistentStore.config @@ -1,17 +1,17 @@ -set (autostart true) -set (preconditions Platform) -set (callsign "org.rdk.PersistentStore") +set(autostart true) +set(preconditions Platform) +set(callsign "org.rdk.PersistentStore") if(PLUGIN_PERSISTENTSTORE_STARTUPORDER) set (startuporder ${PLUGIN_PERSISTENTSTORE_STARTUPORDER}) endif() map() - kv(mode ${PLUGIN_PERSISTENTSTORE_MODE}) -end() -ans(rootobject) - -map() + key(root) + map() + kv(mode ${PLUGIN_PERSISTENTSTORE_MODE}) + kv(locator lib${PLUGIN_IMPLEMENTATION}.so) + end() kv(uri ${PLUGIN_PERSISTENTSTORE_URI}) kv(path ${PLUGIN_PERSISTENTSTORE_PATH}) kv(legacypath ${PLUGIN_PERSISTENTSTORE_LEGACYPATH}) @@ -19,7 +19,6 @@ map() kv(maxsize ${PLUGIN_PERSISTENTSTORE_MAXSIZE}) kv(maxvalue ${PLUGIN_PERSISTENTSTORE_MAXVALUE}) kv(limit ${PLUGIN_PERSISTENTSTORE_LIMIT}) + kv(tokencommand ${PLUGIN_PERSISTENTSTORE_TOKEN_COMMAND}) end() ans(configuration) - -map_append(${configuration} root ${rootobject}) diff --git a/PersistentStore/PersistentStore.cpp b/PersistentStore/PersistentStore.cpp index e867d6379e..4eb15d94b7 100644 --- a/PersistentStore/PersistentStore.cpp +++ b/PersistentStore/PersistentStore.cpp @@ -73,6 +73,7 @@ namespace Plugin { Core::SystemInfo::SetEnvironment(MAXSIZE_ENV, std::to_string(_config.MaxSize.Value())); Core::SystemInfo::SetEnvironment(MAXVALUE_ENV, std::to_string(_config.MaxValue.Value())); Core::SystemInfo::SetEnvironment(LIMIT_ENV, std::to_string(_config.Limit.Value())); + Core::SystemInfo::SetEnvironment(TOKEN_COMMAND_ENV, _config.TokenCommand.Value()); uint32_t connectionId; diff --git a/PersistentStore/PersistentStore.h b/PersistentStore/PersistentStore.h index 41b84512ce..3d97f0b322 100644 --- a/PersistentStore/PersistentStore.h +++ b/PersistentStore/PersistentStore.h @@ -50,6 +50,7 @@ namespace Plugin { Add(_T("maxsize"), &MaxSize); Add(_T("maxvalue"), &MaxValue); Add(_T("limit"), &Limit); + Add(_T("tokencommand"), &TokenCommand); } public: @@ -60,6 +61,7 @@ namespace Plugin { Core::JSON::DecUInt64 MaxSize; Core::JSON::DecUInt64 MaxValue; Core::JSON::DecUInt64 Limit; + Core::JSON::String TokenCommand; }; class Store2Notification : public Exchange::IStore2::INotification { diff --git a/PersistentStore/grpc/Store2.cpp b/PersistentStore/grpc/Store2.cpp new file mode 100644 index 0000000000..dcd1a008e6 --- /dev/null +++ b/PersistentStore/grpc/Store2.cpp @@ -0,0 +1,8 @@ +#include "Store2.h" + +namespace WPEFramework { +namespace Plugin { + class GrpcStore2 : public Grpc::Store2 {}; + SERVICE_REGISTRATION(GrpcStore2, 1, 0); +} // namespace Plugin +} // namespace WPEFramework diff --git a/PersistentStore/grpc/Store2.h b/PersistentStore/grpc/Store2.h new file mode 100644 index 0000000000..9caf1684e6 --- /dev/null +++ b/PersistentStore/grpc/Store2.h @@ -0,0 +1,329 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2022 RDK Management + * + * 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. + */ + +#pragma once + +#include "../Module.h" +#include "secure_storage.grpc.pb.h" +#include +#include +#ifdef WITH_SYSMGR +#include +#include +#endif + +namespace WPEFramework { +namespace Plugin { + namespace Grpc { + + class Store2 : public Exchange::IStore2 { + private: + Store2(const Store2&) = delete; + Store2& operator=(const Store2&) = delete; + + public: + Store2() + : Store2( + getenv(URI_ENV), + getenv(TOKEN_COMMAND_ENV)) + { + } + Store2(const string& uri, const string& tokenCommand) + : IStore2() + , _uri(uri) + , _tokenCommand(tokenCommand) + { + Open(); + } + ~Store2() override = default; + + private: + void Open() + { + std::shared_ptr creds; + if ((_uri.find("localhost") == string::npos) && (_uri.find("0.0.0.0") == string::npos)) { + creds = grpc::SslCredentials(grpc::SslCredentialsOptions()); + } else { + creds = grpc::InsecureChannelCredentials(); + } + _stub = ::distp::gateway::secure_storage::v1::SecureStorageService::NewStub( + grpc::CreateChannel(_uri, creds)); + } + static bool IsTimeSynced() + { +#ifdef WITH_SYSMGR + IARM_Bus_Init(IARM_INIT_NAME); + IARM_Bus_Connect(); + IARM_Bus_SYSMgr_GetSystemStates_Param_t param; + if ((IARM_Bus_Call(IARM_BUS_SYSMGR_NAME, IARM_BUS_SYSMGR_API_GetSystemStates, ¶m, sizeof(param)) != IARM_RESULT_SUCCESS) + || !param.time_source.state) { + return false; + } +#endif + return true; + } + static string ExecuteCmd(const char* cmd) + { + string result; + auto pipe = popen(cmd, "r"); + if (pipe != nullptr) { + char buffer[128]; + while (fgets(buffer, sizeof buffer, pipe) != nullptr) { + result += buffer; + } + pclose(pipe); + } + return result; + } + string GetToken() const + { + class Authorization : public Core::JSON::Container { + public: + Authorization() + : Core::JSON::Container() + , Expires(0) + , Received(0) + { + Add(_T("token"), &Token); + Add(_T("expires"), &Expires); + Add(_T("received"), &Received); + } + Core::JSON::String Token; + Core::JSON::DecUInt64 Expires; + Core::JSON::DecUInt64 Received; + }; + Authorization auth; + auth.FromString(ExecuteCmd(_tokenCommand.c_str())); + return auth.Token.Value(); + } + + public: + uint32_t Register(INotification* notification) override + { + Core::SafeSyncType lock(_clientLock); + + ASSERT(std::find(_clients.begin(), _clients.end(), notification) == _clients.end()); + + notification->AddRef(); + _clients.push_back(notification); + + return Core::ERROR_NONE; + } + uint32_t Unregister(INotification* notification) override + { + Core::SafeSyncType lock(_clientLock); + + std::list::iterator + index(std::find(_clients.begin(), _clients.end(), notification)); + + ASSERT(index != _clients.end()); + + if (index != _clients.end()) { + notification->Release(); + _clients.erase(index); + } + + return Core::ERROR_NONE; + } + + uint32_t SetValue(const ScopeType scope, const string& ns, const string& key, const string& value, const uint32_t ttl) override + { + ASSERT(scope == ScopeType::ACCOUNT); + + uint32_t result; + + grpc::ClientContext context; + if ((_uri.find("localhost") == string::npos) && (_uri.find("0.0.0.0") == string::npos)) { + context.AddMetadata("authorization", "Bearer " + GetToken()); + } + ::distp::gateway::secure_storage::v1::UpdateValueRequest request; + auto v = new ::distp::gateway::secure_storage::v1::Value(); + v->set_value(value); + if (ttl != 0) { + auto t = new google::protobuf::Duration(); + t->set_seconds(ttl); + v->set_allocated_ttl(t); + } + auto k = new ::distp::gateway::secure_storage::v1::Key(); + k->set_app_id(ns); + k->set_key(key); + k->set_scope(::distp::gateway::secure_storage::v1::Scope::SCOPE_ACCOUNT); + v->set_allocated_key(k); + request.set_allocated_value(v); + ::distp::gateway::secure_storage::v1::UpdateValueResponse response; + auto status = _stub->UpdateValue(&context, request, &response); + + if (status.ok()) { + OnValueChanged(ns, key, value); + result = Core::ERROR_NONE; + } else { + OnError(__FUNCTION__, status); + if (status.error_code() == grpc::StatusCode::INVALID_ARGUMENT) { + result = Core::ERROR_INVALID_INPUT_LENGTH; + } else { + result = Core::ERROR_GENERAL; + } + } + + return result; + } + uint32_t GetValue(const ScopeType scope, const string& ns, const string& key, string& value, uint32_t& ttl) override + { + ASSERT(scope == ScopeType::ACCOUNT); + + uint32_t result; + + grpc::ClientContext context; + if ((_uri.find("localhost") == string::npos) && (_uri.find("0.0.0.0") == string::npos)) { + context.AddMetadata("authorization", "Bearer " + GetToken()); + } + ::distp::gateway::secure_storage::v1::GetValueRequest request; + auto k = new ::distp::gateway::secure_storage::v1::Key(); + k->set_app_id(ns); + k->set_key(key); + k->set_scope(::distp::gateway::secure_storage::v1::Scope::SCOPE_ACCOUNT); + request.set_allocated_key(k); + ::distp::gateway::secure_storage::v1::GetValueResponse response; + auto status = _stub->GetValue(&context, request, &response); + + if (status.ok()) { + if (response.has_value()) { + auto v = response.value(); + if (v.has_ttl()) { + ttl = v.ttl().seconds(); + value = v.value(); + result = Core::ERROR_NONE; + } else if (v.has_expire_time() && (v.expire_time().seconds() != 0)) { + if (IsTimeSynced()) { + ttl = v.expire_time().seconds() - time(nullptr); + value = v.value(); + result = Core::ERROR_NONE; + } else { + result = Core::ERROR_PENDING_CONDITIONS; + } + } else { + ttl = 0; + value = v.value(); + result = Core::ERROR_NONE; + } + } else { + result = Core::ERROR_UNKNOWN_KEY; + } + } else { + OnError(__FUNCTION__, status); + if (status.error_code() == grpc::StatusCode::INVALID_ARGUMENT) { + result = Core::ERROR_INVALID_INPUT_LENGTH; + } else { + result = Core::ERROR_GENERAL; + } + } + + return result; + } + uint32_t DeleteKey(const ScopeType scope, const string& ns, const string& key) override + { + ASSERT(scope == ScopeType::ACCOUNT); + + uint32_t result; + + grpc::ClientContext context; + if ((_uri.find("localhost") == string::npos) && (_uri.find("0.0.0.0") == string::npos)) { + context.AddMetadata("authorization", "Bearer " + GetToken()); + } + ::distp::gateway::secure_storage::v1::DeleteValueRequest request; + auto k = new ::distp::gateway::secure_storage::v1::Key(); + k->set_app_id(ns); + k->set_key(key); + k->set_scope(::distp::gateway::secure_storage::v1::Scope::SCOPE_ACCOUNT); + request.set_allocated_key(k); + ::distp::gateway::secure_storage::v1::DeleteValueResponse response; + auto status = _stub->DeleteValue(&context, request, &response); + + if (status.ok()) { + result = Core::ERROR_NONE; + } else { + OnError(__FUNCTION__, status); + if (status.error_code() == grpc::StatusCode::INVALID_ARGUMENT) { + result = Core::ERROR_INVALID_INPUT_LENGTH; + } else { + result = Core::ERROR_GENERAL; + } + } + + return result; + } + uint32_t DeleteNamespace(const ScopeType scope, const string& ns) override + { + ASSERT(scope == ScopeType::ACCOUNT); + + uint32_t result; + + grpc::ClientContext context; + if ((_uri.find("localhost") == string::npos) && (_uri.find("0.0.0.0") == string::npos)) { + context.AddMetadata("authorization", "Bearer " + GetToken()); + } + ::distp::gateway::secure_storage::v1::DeleteAllValuesRequest request; + request.set_app_id(ns); + request.set_scope(::distp::gateway::secure_storage::v1::Scope::SCOPE_ACCOUNT); + ::distp::gateway::secure_storage::v1::DeleteAllValuesResponse response; + auto status = _stub->DeleteAllValues(&context, request, &response); + + if (status.ok()) { + result = Core::ERROR_NONE; + } else { + OnError(__FUNCTION__, status); + result = Core::ERROR_GENERAL; + } + + return result; + } + + BEGIN_INTERFACE_MAP(Store2) + INTERFACE_ENTRY(IStore2) + END_INTERFACE_MAP + + private: + void OnValueChanged(const string& ns, const string& key, const string& value) + { + Core::SafeSyncType lock(_clientLock); + + std::list::iterator + index(_clients.begin()); + + while (index != _clients.end()) { + (*index)->ValueChanged(ScopeType::DEVICE, ns, key, value); + index++; + } + } + void OnError(const char* fn, const grpc::Status& status) const + { + TRACE(Trace::Error, (_T("%s grpc error %d %s %s"), fn, status.error_code(), status.error_message().c_str(), status.error_details().c_str())); + } + + private: + const string _uri; + const string _tokenCommand; + std::unique_ptr<::distp::gateway::secure_storage::v1::SecureStorageService::Stub> _stub; + std::list _clients; + Core::CriticalSection _clientLock; + }; + + } // namespace Grpc +} // namespace Plugin +} // namespace WPEFramework diff --git a/PersistentStore/grpc/l0test/CMakeLists.txt b/PersistentStore/grpc/l0test/CMakeLists.txt new file mode 100644 index 0000000000..8e4aa0ab13 --- /dev/null +++ b/PersistentStore/grpc/l0test/CMakeLists.txt @@ -0,0 +1,69 @@ +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2020 RDK Management +# +# 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. + +cmake_minimum_required(VERSION 3.14) + +project(grpcl0test) + +set(CMAKE_CXX_STANDARD 11) + +add_executable(${PROJECT_NAME} + ../../Module.cpp + Store2Test.cpp +) + +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip +) +FetchContent_MakeAvailable(googletest) +target_link_libraries(${PROJECT_NAME} PRIVATE gmock_main) + +find_package(WPEFramework) +find_package(${NAMESPACE}Plugins REQUIRED) +target_link_libraries(${PROJECT_NAME} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins) + +find_package(Protobuf REQUIRED) +target_link_libraries(${PROJECT_NAME} PRIVATE ${Protobuf_LIBRARIES}) + +add_custom_target(protoc + ${Protobuf_PROTOC_EXECUTABLE} --cpp_out ${CMAKE_CURRENT_BINARY_DIR} -I ${CMAKE_CURRENT_SOURCE_DIR}/../secure_storage ${CMAKE_CURRENT_SOURCE_DIR}/../secure_storage/secure_storage.proto +) +add_dependencies(${PROJECT_NAME} protoc) + +find_package(gRPC CONFIG) +if (gRPC_FOUND) + set(GRPC_LIBS gRPC::grpc++) + set(GRPC_CPP_PLUGIN $) +else () + set(GRPC_LIBS grpc++) + find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) +endif () +target_link_libraries(${PROJECT_NAME} PRIVATE ${GRPC_LIBS}) + +add_custom_target(protoc-gen-grpc + ${Protobuf_PROTOC_EXECUTABLE} --grpc_out ${CMAKE_CURRENT_BINARY_DIR} --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN} -I ${CMAKE_CURRENT_SOURCE_DIR}/../secure_storage ${CMAKE_CURRENT_SOURCE_DIR}/../secure_storage/secure_storage.proto +) +add_dependencies(${PROJECT_NAME} protoc-gen-grpc) + +set(PROTO_SRCS secure_storage.pb.cc secure_storage.grpc.pb.cc) +target_sources(${PROJECT_NAME} PRIVATE ${PROTO_SRCS}) +set_property(SOURCE ${PROTO_SRCS} PROPERTY GENERATED 1) +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +install(TARGETS ${PROJECT_NAME} DESTINATION bin) diff --git a/PersistentStore/grpc/l0test/SecureStorageServiceMock.h b/PersistentStore/grpc/l0test/SecureStorageServiceMock.h new file mode 100644 index 0000000000..3165e289eb --- /dev/null +++ b/PersistentStore/grpc/l0test/SecureStorageServiceMock.h @@ -0,0 +1,14 @@ +#pragma once + +#include "secure_storage.grpc.pb.h" +#include + +class SecureStorageServiceMock : public ::distp::gateway::secure_storage::v1::SecureStorageService::Service { +public: + ~SecureStorageServiceMock() override = default; + MOCK_METHOD(::grpc::Status, GetValue, (::grpc::ServerContext * context, const ::distp::gateway::secure_storage::v1::GetValueRequest* request, ::distp::gateway::secure_storage::v1::GetValueResponse* response), (override)); + MOCK_METHOD(::grpc::Status, UpdateValue, (::grpc::ServerContext * context, const ::distp::gateway::secure_storage::v1::UpdateValueRequest* request, ::distp::gateway::secure_storage::v1::UpdateValueResponse* response), (override)); + MOCK_METHOD(::grpc::Status, DeleteValue, (::grpc::ServerContext * context, const ::distp::gateway::secure_storage::v1::DeleteValueRequest* request, ::distp::gateway::secure_storage::v1::DeleteValueResponse* response), (override)); + MOCK_METHOD(::grpc::Status, DeleteAllValues, (::grpc::ServerContext * context, const ::distp::gateway::secure_storage::v1::DeleteAllValuesRequest* request, ::distp::gateway::secure_storage::v1::DeleteAllValuesResponse* response), (override)); + MOCK_METHOD(::grpc::Status, SeedValue, (::grpc::ServerContext * context, const ::distp::gateway::secure_storage::v1::SeedValueRequest* request, ::distp::gateway::secure_storage::v1::SeedValueResponse* response), (override)); +}; diff --git a/PersistentStore/grpc/l0test/Server.h b/PersistentStore/grpc/l0test/Server.h new file mode 100644 index 0000000000..b7adb02151 --- /dev/null +++ b/PersistentStore/grpc/l0test/Server.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +struct Server { + std::unique_ptr server; + Server(const std::string& uri, grpc::Service* service) + { + grpc::ServerBuilder builder; + builder.AddListeningPort(uri, grpc::InsecureServerCredentials()); + builder.RegisterService(service); + server = builder.BuildAndStart(); + } + ~Server() + { + server->Shutdown(); + } +}; diff --git a/PersistentStore/grpc/l0test/Store2Test.cpp b/PersistentStore/grpc/l0test/Store2Test.cpp new file mode 100644 index 0000000000..a354e79691 --- /dev/null +++ b/PersistentStore/grpc/l0test/Store2Test.cpp @@ -0,0 +1,168 @@ +#include +#include + +#include "../Store2.h" +#include "SecureStorageServiceMock.h" +#include "Server.h" + +using ::distp::gateway::secure_storage::v1::DeleteAllValuesRequest; +using ::distp::gateway::secure_storage::v1::DeleteAllValuesResponse; +using ::distp::gateway::secure_storage::v1::DeleteValueRequest; +using ::distp::gateway::secure_storage::v1::DeleteValueResponse; +using ::distp::gateway::secure_storage::v1::GetValueRequest; +using ::distp::gateway::secure_storage::v1::GetValueResponse; +using ::distp::gateway::secure_storage::v1::Key; +using ::distp::gateway::secure_storage::v1::Scope; +using ::distp::gateway::secure_storage::v1::UpdateValueRequest; +using ::distp::gateway::secure_storage::v1::UpdateValueResponse; +using ::distp::gateway::secure_storage::v1::Value; +using ::testing::_; +using ::testing::Eq; +using ::testing::Gt; +using ::testing::Invoke; +using ::testing::IsFalse; +using ::testing::IsTrue; +using ::testing::Le; +using ::testing::NiceMock; +using ::testing::Test; +using ::WPEFramework::Core::Time; +using ::WPEFramework::Exchange::IStore2; +using ::WPEFramework::Plugin::Grpc::Store2; + +const auto kUri = "0.0.0.0:50051"; +const auto kTokenCommand = ""; +const auto kValue = "value_1"; +const auto kKey = "key_1"; +const auto kAppId = "app_id_1"; +const auto kTtl = 100; +const auto kScope = Scope::SCOPE_ACCOUNT; + +class AStore2 : public Test { +protected: + NiceMock service; + Server server; + WPEFramework::Core::ProxyType store2; + AStore2() + : server(kUri, &service) + , store2(WPEFramework::Core::ProxyType::Create(kUri, kTokenCommand)) + { + } +}; + +TEST_F(AStore2, GetsValueWithTtl) +{ + GetValueRequest req; + ON_CALL(service, GetValue(_, _, _)) + .WillByDefault(Invoke( + [&](::grpc::ServerContext*, const GetValueRequest* request, GetValueResponse* response) { + req = (*request); + auto v = new Value(); + v->set_value(kValue); + auto t = new google::protobuf::Duration(); + t->set_seconds(kTtl); + v->set_allocated_ttl(t); + auto k = new Key(); + k->set_key(request->key().key()); + k->set_app_id(request->key().app_id()); + k->set_scope(request->key().scope()); + v->set_allocated_key(k); + response->set_allocated_value(v); + return grpc::Status::OK; + })); + + string v; + uint32_t t; + ASSERT_THAT(store2->GetValue(IStore2::ScopeType::ACCOUNT, kAppId, kKey, v, t), Eq(WPEFramework::Core::ERROR_NONE)); + ASSERT_THAT(req.has_key(), IsTrue()); + EXPECT_THAT(req.key().key(), Eq(kKey)); + EXPECT_THAT(req.key().app_id(), Eq(kAppId)); + EXPECT_THAT(req.key().scope(), Eq(kScope)); + EXPECT_THAT(v, Eq(kValue)); + EXPECT_THAT(t, Eq(kTtl)); +} + +TEST_F(AStore2, GetsValueWithExpireTime) +{ + GetValueRequest req; + ON_CALL(service, GetValue(_, _, _)) + .WillByDefault(Invoke( + [&](::grpc::ServerContext*, const GetValueRequest* request, GetValueResponse* response) { + req = (*request); + auto v = new Value(); + v->set_value(kValue); + auto t = new google::protobuf::Timestamp(); + t->set_seconds(kTtl + (Time::Now().Ticks() / Time::TicksPerMillisecond / 1000)); + v->set_allocated_expire_time(t); + auto k = new Key(); + k->set_key(request->key().key()); + k->set_app_id(request->key().app_id()); + k->set_scope(request->key().scope()); + v->set_allocated_key(k); + response->set_allocated_value(v); + return grpc::Status::OK; + })); + + string v; + uint32_t t; + ASSERT_THAT(store2->GetValue(IStore2::ScopeType::ACCOUNT, kAppId, kKey, v, t), Eq(WPEFramework::Core::ERROR_NONE)); + ASSERT_THAT(req.has_key(), IsTrue()); + EXPECT_THAT(req.key().key(), Eq(kKey)); + EXPECT_THAT(req.key().app_id(), Eq(kAppId)); + EXPECT_THAT(req.key().scope(), Eq(kScope)); + EXPECT_THAT(v, Eq(kValue)); + EXPECT_THAT(t, Le(kTtl)); + EXPECT_THAT(t, Gt(0)); +} + +TEST_F(AStore2, SetsValueWithTtl) +{ + UpdateValueRequest req; + ON_CALL(service, UpdateValue(_, _, _)) + .WillByDefault(Invoke( + [&](::grpc::ServerContext*, const UpdateValueRequest* request, UpdateValueResponse*) { + req = (*request); + return grpc::Status::OK; + })); + + ASSERT_THAT(store2->SetValue(IStore2::ScopeType::ACCOUNT, kAppId, kKey, kValue, kTtl), Eq(WPEFramework::Core::ERROR_NONE)); + ASSERT_THAT(req.has_value(), IsTrue()); + EXPECT_THAT(req.value().value(), Eq(kValue)); + ASSERT_THAT(req.value().has_key(), IsTrue()); + EXPECT_THAT(req.value().key().key(), Eq(kKey)); + EXPECT_THAT(req.value().key().app_id(), Eq(kAppId)); + EXPECT_THAT(req.value().key().scope(), Eq(kScope)); + ASSERT_THAT(req.value().has_ttl(), IsTrue()); + EXPECT_THAT(req.value().ttl().seconds(), Eq(kTtl)); +} + +TEST_F(AStore2, DeletesKey) +{ + DeleteValueRequest req; + ON_CALL(service, DeleteValue(_, _, _)) + .WillByDefault(Invoke( + [&](::grpc::ServerContext*, const DeleteValueRequest* request, DeleteValueResponse*) { + req = (*request); + return grpc::Status::OK; + })); + + ASSERT_THAT(store2->DeleteKey(IStore2::ScopeType::ACCOUNT, kAppId, kKey), Eq(WPEFramework::Core::ERROR_NONE)); + ASSERT_THAT(req.has_key(), IsTrue()); + EXPECT_THAT(req.key().key(), Eq(kKey)); + EXPECT_THAT(req.key().app_id(), Eq(kAppId)); + EXPECT_THAT(req.key().scope(), Eq(kScope)); +} + +TEST_F(AStore2, DeletesNamespace) +{ + DeleteAllValuesRequest req; + ON_CALL(service, DeleteAllValues(_, _, _)) + .WillByDefault(Invoke( + [&](::grpc::ServerContext*, const DeleteAllValuesRequest* request, DeleteAllValuesResponse*) { + req = (*request); + return grpc::Status::OK; + })); + + ASSERT_THAT(store2->DeleteNamespace(IStore2::ScopeType::ACCOUNT, kAppId), Eq(WPEFramework::Core::ERROR_NONE)); + ASSERT_THAT(req.app_id(), Eq(kAppId)); + EXPECT_THAT(req.scope(), Eq(kScope)); +} diff --git a/PersistentStore/grpc/l2test/CMakeLists.txt b/PersistentStore/grpc/l2test/CMakeLists.txt new file mode 100644 index 0000000000..24b36fcab7 --- /dev/null +++ b/PersistentStore/grpc/l2test/CMakeLists.txt @@ -0,0 +1,64 @@ +# If not stated otherwise in this file or this component's LICENSE file the +# following copyright and licenses apply: +# +# Copyright 2020 RDK Management +# +# 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. + +cmake_minimum_required(VERSION 3.14) + +project(grpcl2test) + +set(CMAKE_CXX_STANDARD 11) + +add_executable(${PROJECT_NAME} + StubTest.cpp +) + +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip +) +FetchContent_MakeAvailable(googletest) +target_link_libraries(${PROJECT_NAME} PRIVATE gmock_main) + +find_package(Protobuf REQUIRED) +target_link_libraries(${PROJECT_NAME} PRIVATE ${Protobuf_LIBRARIES}) + +add_custom_target(protoc + ${Protobuf_PROTOC_EXECUTABLE} --cpp_out ${CMAKE_CURRENT_BINARY_DIR} -I ${CMAKE_CURRENT_SOURCE_DIR}/../secure_storage ${CMAKE_CURRENT_SOURCE_DIR}/../secure_storage/secure_storage.proto +) +add_dependencies(${PROJECT_NAME} protoc) + +find_package(gRPC CONFIG) +if (gRPC_FOUND) + set(GRPC_LIBS gRPC::grpc++) + set(GRPC_CPP_PLUGIN $) +else () + set(GRPC_LIBS grpc++) + find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin REQUIRED) +endif () +target_link_libraries(${PROJECT_NAME} PRIVATE ${GRPC_LIBS}) + +add_custom_target(protoc-gen-grpc + ${Protobuf_PROTOC_EXECUTABLE} --grpc_out ${CMAKE_CURRENT_BINARY_DIR} --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN} -I ${CMAKE_CURRENT_SOURCE_DIR}/../secure_storage ${CMAKE_CURRENT_SOURCE_DIR}/../secure_storage/secure_storage.proto +) +add_dependencies(${PROJECT_NAME} protoc-gen-grpc) + +set(PROTO_SRCS secure_storage.pb.cc secure_storage.grpc.pb.cc) +target_sources(${PROJECT_NAME} PRIVATE ${PROTO_SRCS}) +set_property(SOURCE ${PROTO_SRCS} PROPERTY GENERATED 1) +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +install(TARGETS ${PROJECT_NAME} DESTINATION bin) diff --git a/PersistentStore/grpc/l2test/StubTest.cpp b/PersistentStore/grpc/l2test/StubTest.cpp new file mode 100644 index 0000000000..e90b1aed3f --- /dev/null +++ b/PersistentStore/grpc/l2test/StubTest.cpp @@ -0,0 +1,396 @@ +#include +#include + +#include "secure_storage.grpc.pb.h" +#include + +using ::distp::gateway::secure_storage::v1::DeleteAllValuesRequest; +using ::distp::gateway::secure_storage::v1::DeleteAllValuesResponse; +using ::distp::gateway::secure_storage::v1::DeleteValueRequest; +using ::distp::gateway::secure_storage::v1::DeleteValueResponse; +using ::distp::gateway::secure_storage::v1::GetValueRequest; +using ::distp::gateway::secure_storage::v1::GetValueResponse; +using ::distp::gateway::secure_storage::v1::Key; +using ::distp::gateway::secure_storage::v1::Scope; +using ::distp::gateway::secure_storage::v1::SecureStorageService; +using ::distp::gateway::secure_storage::v1::UpdateValueRequest; +using ::distp::gateway::secure_storage::v1::UpdateValueResponse; +using ::distp::gateway::secure_storage::v1::Value; +using ::testing::Eq; +using ::testing::Gt; +using ::testing::IsFalse; +using ::testing::IsTrue; +using ::testing::Le; +using ::testing::Test; + +const auto kUri = "ss.eu.prod.developer.comcast.com:443"; +const auto kValue = "value_1"; +const auto kKey = "key_1"; +const auto kAppId = "app_id_1"; +const auto kTtl = 2; +const auto kScope = Scope::SCOPE_ACCOUNT; +const auto kEmpty = ""; +const auto kUnknown = "unknown"; +const auto kToken = "Bearer TOKEN"; + +class AStub : public Test { +protected: + std::unique_ptr stub; + AStub() + : stub(SecureStorageService::NewStub(grpc::CreateChannel( + kUri, + grpc::SslCredentials(grpc::SslCredentialsOptions())))) + { + } +}; + +TEST_F(AStub, DoesNotUpdateValueWhenAppIdEmpty) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + UpdateValueRequest request; + auto v = new Value(); + v->set_value(kValue); + auto k = new Key(); + k->set_app_id(kEmpty); + k->set_key(kKey); + k->set_scope(kScope); + v->set_allocated_key(k); + request.set_allocated_value(v); + UpdateValueResponse response; + auto status = stub->UpdateValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsFalse()); + EXPECT_THAT(status.error_code(), Eq(3)); + EXPECT_THAT(status.error_message(), Eq("app_id is mandatory")); +} + +TEST_F(AStub, DoesNotUpdateValueWhenKeyEmpty) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + UpdateValueRequest request; + auto v = new Value(); + v->set_value(kValue); + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kEmpty); + k->set_scope(kScope); + v->set_allocated_key(k); + request.set_allocated_value(v); + UpdateValueResponse response; + auto status = stub->UpdateValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsFalse()); + EXPECT_THAT(status.error_code(), Eq(3)); + EXPECT_THAT(status.error_message(), Eq("invalid UpdateValueRequest.Value: embedded message failed validation | caused by: invalid Value.Key: embedded message failed validation | caused by: invalid Key.Key: value length must be at least 1 runes")); +} + +TEST_F(AStub, DoesNotGetValueWhenAppIdEmpty) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + GetValueRequest request; + auto k = new Key(); + k->set_app_id(kEmpty); + k->set_key(kKey); + k->set_scope(kScope); + request.set_allocated_key(k); + GetValueResponse response; + auto status = stub->GetValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsFalse()); + EXPECT_THAT(status.error_code(), Eq(3)); + EXPECT_THAT(status.error_message(), Eq("app_id is mandatory")); +} + +TEST_F(AStub, DoesNotGetValueWhenKeyEmpty) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + GetValueRequest request; + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kEmpty); + k->set_scope(kScope); + request.set_allocated_key(k); + GetValueResponse response; + auto status = stub->GetValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsFalse()); + EXPECT_THAT(status.error_code(), Eq(3)); + EXPECT_THAT(status.error_message(), Eq("invalid GetValueRequest.Key: embedded message failed validation | caused by: invalid Key.Key: value length must be at least 1 runes")); +} + +TEST_F(AStub, DoesNotDeleteValueWhenAppIdEmpty) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + DeleteValueRequest request; + auto k = new Key(); + k->set_app_id(kEmpty); + k->set_key(kKey); + k->set_scope(kScope); + request.set_allocated_key(k); + DeleteValueResponse response; + auto status = stub->DeleteValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsFalse()); + EXPECT_THAT(status.error_code(), Eq(3)); + EXPECT_THAT(status.error_message(), Eq("app_id is mandatory")); +} + +TEST_F(AStub, DoesNotDeleteValueWhenKeyEmpty) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + DeleteValueRequest request; + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kEmpty); + k->set_scope(kScope); + request.set_allocated_key(k); + DeleteValueResponse response; + auto status = stub->DeleteValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsFalse()); + EXPECT_THAT(status.error_code(), Eq(3)); + EXPECT_THAT(status.error_message(), Eq("invalid DeleteValueRequest.Key: embedded message failed validation | caused by: invalid Key.Key: value length must be at least 1 runes")); +} + +TEST_F(AStub, DeletesAllValuesWhenAppIdEmpty) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + DeleteAllValuesRequest request; + request.set_app_id(kEmpty); + request.set_scope(kScope); + DeleteAllValuesResponse response; + auto status = stub->DeleteAllValues(&context, request, &response); + EXPECT_THAT(status.ok(), IsTrue()); +} + +TEST_F(AStub, GetsValueWhenValueEmpty) +{ + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + UpdateValueRequest request; + auto v = new Value(); + v->set_value(kEmpty); + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kKey); + k->set_scope(kScope); + v->set_allocated_key(k); + request.set_allocated_value(v); + UpdateValueResponse response; + auto status = stub->UpdateValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + } + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + GetValueRequest request; + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kKey); + k->set_scope(kScope); + request.set_allocated_key(k); + GetValueResponse response; + auto status = stub->GetValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + ASSERT_THAT(response.has_value(), IsTrue()); + EXPECT_THAT(response.value().value(), Eq(kEmpty)); + EXPECT_THAT(response.value().has_ttl(), IsFalse()); + ASSERT_THAT(response.value().has_expire_time(), IsTrue()); + EXPECT_THAT(response.value().expire_time().seconds(), Eq(0)); + } +} + +TEST_F(AStub, DoesNotGetValueWhenAppIdUnknown) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + GetValueRequest request; + auto k = new Key(); + k->set_app_id(kUnknown); + k->set_key(kKey); + k->set_scope(kScope); + request.set_allocated_key(k); + GetValueResponse response; + auto status = stub->GetValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + EXPECT_THAT(response.has_value(), IsFalse()); +} + +TEST_F(AStub, DoesNotGetValueWhenKeyUnknown) +{ + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + UpdateValueRequest request; + auto v = new Value(); + v->set_value(kValue); + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kKey); + k->set_scope(kScope); + v->set_allocated_key(k); + request.set_allocated_value(v); + UpdateValueResponse response; + auto status = stub->UpdateValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + } + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + GetValueRequest request; + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kUnknown); + k->set_scope(kScope); + request.set_allocated_key(k); + GetValueResponse response; + auto status = stub->GetValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + EXPECT_THAT(response.has_value(), IsFalse()); + } +} + +TEST_F(AStub, DeletesValueWhenAppIdUnknown) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + DeleteValueRequest request; + auto k = new Key(); + k->set_app_id(kUnknown); + k->set_key(kKey); + k->set_scope(kScope); + request.set_allocated_key(k); + DeleteValueResponse response; + auto status = stub->DeleteValue(&context, request, &response); + EXPECT_THAT(status.ok(), IsTrue()); +} + +TEST_F(AStub, DeletesValueWhenKeyUnknown) +{ + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + UpdateValueRequest request; + auto v = new Value(); + v->set_value(kValue); + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kKey); + k->set_scope(kScope); + v->set_allocated_key(k); + request.set_allocated_value(v); + UpdateValueResponse response; + auto status = stub->UpdateValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + } + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + DeleteValueRequest request; + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kUnknown); + k->set_scope(kScope); + request.set_allocated_key(k); + DeleteValueResponse response; + auto status = stub->DeleteValue(&context, request, &response); + EXPECT_THAT(status.ok(), IsTrue()); + } +} + +TEST_F(AStub, DeletesAllValuesWhenAppIdUnknown) +{ + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + DeleteAllValuesRequest request; + request.set_app_id(kUnknown); + request.set_scope(kScope); + DeleteAllValuesResponse response; + auto status = stub->DeleteAllValues(&context, request, &response); + EXPECT_THAT(status.ok(), IsTrue()); +} + +TEST_F(AStub, GetsValueWhenTtlDidNotExpire) +{ + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + UpdateValueRequest request; + auto v = new Value(); + v->set_value(kValue); + auto t = new ::google::protobuf::Duration(); + t->set_seconds(kTtl); + v->set_allocated_ttl(t); + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kKey); + k->set_scope(kScope); + v->set_allocated_key(k); + request.set_allocated_value(v); + UpdateValueResponse response; + auto status = stub->UpdateValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + } + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + GetValueRequest request; + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kKey); + k->set_scope(kScope); + request.set_allocated_key(k); + GetValueResponse response; + auto status = stub->GetValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + ASSERT_THAT(response.has_value(), IsTrue()); + EXPECT_THAT(response.value().value(), Eq(kValue)); + EXPECT_THAT(response.value().has_ttl(), IsFalse()); + ASSERT_THAT(response.value().has_expire_time(), IsTrue()); + ::google::protobuf::Timestamp now; + now.set_seconds(time(nullptr)); + now.set_nanos(0); + EXPECT_THAT(response.value().expire_time().seconds(), Gt(now.seconds())); + EXPECT_THAT(response.value().expire_time().seconds(), Le(now.seconds() + kTtl)); + } +} + +TEST_F(AStub, DoesNotGetValueWhenTtlExpired) +{ + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + UpdateValueRequest request; + auto v = new Value(); + v->set_value(kValue); + auto t = new ::google::protobuf::Duration(); + t->set_seconds(kTtl); + v->set_allocated_ttl(t); + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kKey); + k->set_scope(kScope); + v->set_allocated_key(k); + request.set_allocated_value(v); + UpdateValueResponse response; + auto status = stub->UpdateValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + } + sleep(kTtl); + { + grpc::ClientContext context; + context.AddMetadata("authorization", std::string(kToken)); + GetValueRequest request; + auto k = new Key(); + k->set_app_id(kAppId); + k->set_key(kKey); + k->set_scope(kScope); + request.set_allocated_key(k); + GetValueResponse response; + auto status = stub->GetValue(&context, request, &response); + ASSERT_THAT(status.ok(), IsTrue()); + EXPECT_THAT(response.has_value(), IsFalse()); + } +} diff --git a/PersistentStore/grpc/secure_storage/secure_storage.proto b/PersistentStore/grpc/secure_storage/secure_storage.proto new file mode 100644 index 0000000000..526eefb8eb --- /dev/null +++ b/PersistentStore/grpc/secure_storage/secure_storage.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +package distp.gateway.secure_storage.v1; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; + +// SecureStorageService handles the storage and deletion of data (represented as string values) from applications given a particular key and scope. +service SecureStorageService { + // GetValue retrieves the value string stored in SecureStorage based on the scope and key provided for the current application. + rpc GetValue(GetValueRequest) returns (GetValueResponse); + // UpdateValue stores the string value provided in SecureStorage against the scope and key provided for the current application. + rpc UpdateValue(UpdateValueRequest) returns (UpdateValueResponse); + // DeleteValue removes the string value stored in SecureStorage, if present, defined by the scope and key provided for the current application/device. + rpc DeleteValue(DeleteValueRequest) returns (DeleteValueResponse); + // DeleteAllValues removes all values stored against the provided application for the given account. This includes all key/value pairs + // stored against the device scope for other devices within the account. Please note that this method does not take a key as input. + // Also, DeleteAllValues is a separate method given that access to delete all values may be controlled by explicit capabilities. + rpc DeleteAllValues(DeleteAllValuesRequest) returns (DeleteAllValuesResponse); + // SeedValue stores the string value provided in SecureStorage against the scope and key provided for the current application. + // This is a management API so the stored value will not cause the distributor's storage limits to be exceeded. + rpc SeedValue(SeedValueRequest) returns (SeedValueResponse); +} + +// Key is a group of fields that contribute to building the composite key that will be used to store the data in SecureStorage. +message Key { + // key of the key,value pair to be retrieved from SecureStorage. + string key = 1; + // Scope describes the extent of the key,value pair. Scope is determined by the distributor. + Scope scope = 2; + + // app_id is the unique identifier of an app or family of apps. + string app_id = 3; +} + +// Value contains the value of the requested data as well as some other relevant fields like scope and expiry useful for the client. +message Value { + // key of the key,value pair that was retrieved from SecureStorage. + Key key = 1; + // value is the value associated with the key,value pair retrieved from SecureStorage. + string value = 2; + // expiration returns the expire time of the retrieved value. Conforms to AIP-214. + oneof expiration { + // Timestamp in UTC of when this resource is considered expired. + // This is *always* provided on output, regardless of what was sent on input. + google.protobuf.Timestamp expire_time = 3; + + // Input only. The TTL for this resource. + google.protobuf.Duration ttl = 4; + } +} + +// GetValueRequest is the request to retrieve the SecureStorage data. +message GetValueRequest { + // key is the group of fields that contribute to building the composite key that will be used to store the data in SecureStorage. + Key key = 1; +} + +// GetValueResponse is the response containing the value of the requested key within the specified scope. +message GetValueResponse { + // value contains the data associated with the key,value pair that was stored in SecureStorage. + Value value = 1; +} + +// UpdateValueRequest is the request to store data in SecureStorage. +message UpdateValueRequest { + // key is the group of fields that contribute to building the composite key that will be used to store the data in SecureStorage. + Value value = 1; +} + +// UpdateValueResponse is the response from the GetValue method. +message UpdateValueResponse {} + +// DeleteValueRequest is the request to remove a stored value given the key and scope. +message DeleteValueRequest { + // key is the group of fields that contribute to building the composite key that will be used to store the data in SecureStorage. + Key key = 1; +} + +// DeleteValueResponse is the response from the DeleteValue method. +message DeleteValueResponse {} + +// DeleteAllValuesRequest is the request to delete all of the keys associated with an app under the given account. +message DeleteAllValuesRequest { + // app_id is the unique identifier of an app or family of apps. + string app_id = 1; + // Scope describes the extent of the key,value pair. Scope is determined by the distributor. + Scope scope = 2; +} + +// DeleteAllValuesResponse is the response from the DeleteAllValues method. +message DeleteAllValuesResponse {} + +// SeedValueRequest is the request to store data in SecureStorage. Stored data will not cause the distributor's storage limits to be exceeded. +message SeedValueRequest { + // value contains the data associated with the key,value pair that will be stored in SecureStorage. + Value value = 1; +} + +// SeedValueResponse is the response from the SeedValue method. +message SeedValueResponse {} + +// Enumerated values for scope. +enum Scope { + // Represents an unset or invalid scope. + SCOPE_UNSPECIFIED = 0; + // Account scope. + SCOPE_ACCOUNT = 1; + // Device scope. + SCOPE_DEVICE = 2; +}