From fdfc53673e2705c5dd1f0462f90f8467d0ba686b Mon Sep 17 00:00:00 2001 From: Timm Fitschen <t.fitschen@indiscale.com> Date: Mon, 19 Jul 2021 20:23:12 +0200 Subject: [PATCH] add configuration manager --- .docker/Dockerfile | 5 +- CMakeLists.txt | 4 +- README_SETUP.md | 18 ++ caosdb-client-configuration-schema.json | 86 ++++++ conanfile.py | 1 + doc/CMakeLists.txt | 11 +- doc/capi/index.rst.in | 2 +- doc/conf.py.in | 4 +- doc/cppapi/index.rst.in | 2 +- doc/index.rst.in | 3 - doc/requirements.txt | 27 -- include/CMakeLists.txt | 1 + include/caosdb/authentication.h | 1 + include/caosdb/configuration.h | 221 ++++++++++++++ include/caosdb/connection.h | 80 ++++- include/caosdb/constants.h.in | 15 + include/caosdb/exceptions.h | 18 ++ include/caosdb/utility.h | 6 + include/ccaosdb.h | 16 + requirements.txt | 3 + src/CMakeLists.txt | 1 + src/caosdb/configuration.cpp | 289 ++++++++++++++++++ src/caosdb/connection.cpp | 60 +++- src/ccaosdb.cpp | 22 +- src/cxxcaosdbcli.cpp | 42 +-- test/CMakeLists.txt | 1 + test/caosdb_test_utility.h.in | 12 + test/test_ccaosdb.cpp | 32 +- test/test_configuration.cpp | 79 +++++ test/test_connection.cpp | 43 ++- ..._broken_caosdb_client_no_connections1.json | 2 + ..._broken_caosdb_client_no_connections2.json | 3 + ..._broken_caosdb_client_no_connections3.json | 4 + test/test_data/test_caosdb_client.json | 28 ++ 34 files changed, 1039 insertions(+), 103 deletions(-) create mode 100644 caosdb-client-configuration-schema.json delete mode 100644 doc/requirements.txt create mode 100644 include/caosdb/configuration.h create mode 100644 src/caosdb/configuration.cpp create mode 100644 test/test_configuration.cpp create mode 100644 test/test_data/test_broken_caosdb_client_no_connections1.json create mode 100644 test/test_data/test_broken_caosdb_client_no_connections2.json create mode 100644 test/test_data/test_broken_caosdb_client_no_connections3.json create mode 100644 test/test_data/test_caosdb_client.json diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 638a46a..ef503a9 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,5 @@ -FROM debian:latest +FROM debian:buster-backports -RUN echo "deb http://deb.debian.org/debian buster-backports main" > /etc/apt/sources.list.d/buster-backports.list RUN apt-get update RUN apt-get install -y cmake/buster-backports RUN apt-get install -y lcov @@ -14,8 +13,6 @@ RUN apt-get install -y openjdk-11-jdk-headless WORKDIR / -COPY doc/requirements.txt doc-requirements.txt -RUN pip3 install -r doc-requirements.txt COPY requirements.txt build-requirements.txt RUN pip3 install -r build-requirements.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 288c546..91c0423 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -230,7 +230,7 @@ if(_LINTING) else() message(STATUS "clang-tidy: ${clang_tidy}") set(_CMAKE_CXX_CLANG_TIDY_CHECKS - "--checks=*,-fuchsia-*,-llvm-include-order,-llvmlibc-*") + "--checks=*,-fuchsia-*,-llvm-include-order,-llvmlibc-*,-readability-convert-member-functions-to-static,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-hicpp-no-array-decay,-llvm-else-after-return,-readability-else-after-return") set(_CMAKE_CXX_CLANG_TIDY "${clang_tidy}" "--header-filter=caosdb/.*[^\(\.pb\.h\)]$" "--warnings-as-errors=*") @@ -338,7 +338,7 @@ install(FILES ${PROJECT_SOURCE_DIR}/caosdbConfigVersion.cmake ####################################################### option(AUTOFORMATTING "call clang-format at configure time" ON) if(AUTOFORMATTING) - file(GLOB format_test_sources test/*.cpp test/*.h) + file(GLOB format_test_sources test/*.cpp test/*.h test/*.h.in) execute_process(COMMAND clang-format -i --verbose ${libcaosdb_INCL} ${libcaosdb_SRC} ${libcaosdb_TEST_SRC} ${PROJECT_SOURCE_DIR}/src/cxxcaosdbcli.cpp diff --git a/README_SETUP.md b/README_SETUP.md index c68df92..e877556 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -52,6 +52,24 @@ The coverage report can be viewed in a browser by opening Please adhere to [Google's C++ naming conventions](https://google.github.io/styleguide/cppguide.html#Naming). +## Client Configuration + +You can use a json file for the configuration of the client. See +`test/test_data/test_caosdb_client.json` for an example. You may use +`caosdb-client-configuration-schema.json` to validate your schema. + +The client will load the configuration file from the first existing file in the following locations (predendence from highest to lowest): + +1. A file specified by the environment variable `$CAOSDB_CLIENT_CONFIGURATION`. +2. `$PWD/caosdb_client.json` +3. `$PWD/caosdb-client.json` +4. `$PWD/.caosdb_client.json` +5. `$PWD/.caosdb-client.json` +6. `$HOME/caosdb_client.json` +7. `$HOME/caosdb-client.json` +8. `$HOME/.caosdb_client.json` +9. `$HOME/.caosdb-client.json` + ## Documentation In the build directory, run diff --git a/caosdb-client-configuration-schema.json b/caosdb-client-configuration-schema.json new file mode 100644 index 0000000..882f757 --- /dev/null +++ b/caosdb-client-configuration-schema.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CaosDB Client Configuration", + "description": "Configuration of the connection, logging, and other things of a CaosDB client.", + "type": "object", + "additionalProperties": false, + "properties": { + "connections": { + "type": "object", + "description": "Named connection configurations.", + "properties": { + "default": { + "oneOf": [ + { + "type": "string", + "description": "Name of the default connection." + }, { + "$ref": "#/definitions/connection_configuration" + } + ] + } + }, + "additionalProperties": { + "$ref": "#/definitions/connection_configuration" + } + }, + "extension": { + "type": "object", + "description": "A reserved configuration object which may be used to store additional options for particular clients and special extensions.", + "additionalProperties": true + } + }, + "definitions": { + "connection_configuration": { + "type": "object", + "description": "A single connection configuration.", + "additionalProperties": false, + "properties": { + "name": { "type": "string" }, + "host": { + "type": "string", + "description": "Domain name or ip address of the server host.", + "default": "localhost", + "examples": ["localhost", "caosdb.example.com", "192.168.0.123"] + }, + "port": { + "type": "integer", + "description": "Ip port of the grpc end-point of the CaosDB server.", + "mininum": 1, + "default": 8443, + "maximum": 65535 + }, + "tls": { + "type": "boolean", + "description": "Indicates that the connection is using TLS (transport layer security) for authentication of the server and encryption of the communication. Setting this to 'false' is insecure unless other mechanism are in place to prevent a man-in-the-middle attack and eavesdropping by an unauthorized agent.", + "default": true + }, + "server_certificate_path": { + "type": "string", + "description": "Relative or absolute path to a public certificate of a trusted CA (certificate authority) in PEM format. If not specified, the client system's default directory for certificates will be scanned (that is '/etc/ssl/certs/' in many linux distros).", + "default": null + }, + "authentication": { + "type": "object", + "description": "Configuration of the user authentication. If the authentication property is not set, the client attempts to connect to the server as an anonymous user.", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": ["plain"] + }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "auth_token": { "type": "string" } + }, + "allOf": [ + { + "if": {"properties": {"type": { "pattern": "^plain$" } } }, + "then": {"required": ["username", "password"] } + } + ] + } + } + } + } +} diff --git a/conanfile.py b/conanfile.py index c8bba23..1ed1abd 100644 --- a/conanfile.py +++ b/conanfile.py @@ -20,6 +20,7 @@ class CaosdbConan(ConanFile): def config_options(self): if self.settings.os == "Windows": del self.options.fPIC + self.options["boost"].without_python = True # def source(self): # self.run("git clone https://gitlab.indiscale.com/caosdb/src/caosdb-cpplib.git") diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 090df1c..76d7dad 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -21,8 +21,8 @@ find_package(Doxygen) if (DOXYGEN_FOUND) - string(REPLACE ";" " " DOXYGEN_INPUT "${PROJECT_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/include") - string(REPLACE ";" " " DOXYGEN_STRIP_FROM_PATH "${PROJECT_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/include") + string(REPLACE ";" " " DOXYGEN_INPUT + "${libcaosdb_INCL} ${PROJECT_INCLUDE_DIR}/ccaosdb.h") configure_file(Doxyfile.in Doxyfile) @@ -56,9 +56,14 @@ if (DOXYGEN_FOUND) "" HEADER_FILE_NAME ${HEADER_FILE_NAME}) + string(REPLACE + "caosdb/" + "" + DOC_FILE_NAME + ${HEADER_FILE_NAME}) configure_file( header_file.rst.in - cppapi/_${HEADER_FILE_NAME}.rst) + cppapi/_${DOC_FILE_NAME}.rst) endforeach () # create (plain) C docs diff --git a/doc/capi/index.rst.in b/doc/capi/index.rst.in index b4fe4a8..887f7ee 100644 --- a/doc/capi/index.rst.in +++ b/doc/capi/index.rst.in @@ -19,7 +19,7 @@ # along with this program. If not, see <https://www.gnu.org/licenses/>. # -.. _api_root: +.. _capi_root: C API ===== diff --git a/doc/conf.py.in b/doc/conf.py.in index bdc3216..f04a7a8 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -20,10 +20,12 @@ project = '@CMAKE_PROJECT_NAME@' copyright = '2021 IndiScale GmbH' author = 'Timm Fitschen' +version = '@CMAKE_PROJECT_VERSION@' +release = '@CMAKE_PROJECT_VERSION@' + rst_prolog = """ .. |PROJECT_NAME| replace:: @CMAKE_PROJECT_NAME@ -.. |PROJECT_VERSION| replace:: @CMAKE_PROJECT_VERSION@ """ diff --git a/doc/cppapi/index.rst.in b/doc/cppapi/index.rst.in index f0639ce..c9780a9 100644 --- a/doc/cppapi/index.rst.in +++ b/doc/cppapi/index.rst.in @@ -19,7 +19,7 @@ # along with this program. If not, see <https://www.gnu.org/licenses/>. # -.. _api_root: +.. _cppapi_root: C++ API ======= diff --git a/doc/index.rst.in b/doc/index.rst.in index a406160..e3fac24 100644 --- a/doc/index.rst.in +++ b/doc/index.rst.in @@ -25,11 +25,8 @@ Welcome to |PROJECT_NAME|'s documentation! ========================================== -Version: |PROJECT_VERSION| - This is work in progress. - .. toctree:: :maxdepth: 4 :caption: Contents: diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index 491959d..0000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,27 +0,0 @@ -alabaster==0.7.12 -Babel==2.9.1 -breathe==4.30.0 -certifi==2020.12.5 -chardet==4.0.0 -docutils==0.16 -idna==2.10 -imagesize==1.2.0 -Jinja2==2.11.3 -MarkupSafe==1.1.1 -packaging==20.9 -Pygments==2.9.0 -pyparsing==2.4.7 -pytz==2021.1 -requests==2.25.1 -six==1.16.0 -snowballstemmer==2.1.0 -Sphinx==4.0.1 -sphinx-rtd-theme==0.5.2 -sphinx-sitemap==2.2.0 -sphinxcontrib-applehelp==1.0.2 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==1.0.3 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.4 -urllib3==1.26.4 diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index dcc501e..48f470b 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -21,6 +21,7 @@ # add all header files to this list set(libcaosdb_INCL ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/authentication.h + ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/configuration.h ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/connection.h ${CMAKE_CURRENT_BINARY_DIR}/caosdb/constants.h ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/entity.h diff --git a/include/caosdb/authentication.h b/include/caosdb/authentication.h index 6b26dd6..8071414 100644 --- a/include/caosdb/authentication.h +++ b/include/caosdb/authentication.h @@ -51,6 +51,7 @@ using grpc::string_ref; */ class Authenticator { public: + virtual ~Authenticator() = default; [[nodiscard]] virtual auto GetCallCredentials() const -> std::shared_ptr<grpc::CallCredentials> = 0; }; diff --git a/include/caosdb/configuration.h b/include/caosdb/configuration.h new file mode 100644 index 0000000..4e4de9a --- /dev/null +++ b/include/caosdb/configuration.h @@ -0,0 +1,221 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +#ifndef CAOSDB_CONFIGURATION_H +#define CAOSDB_CONFIGURATION_H +#include <memory> // for unique_ptr +#include <string> // for string +#include "boost/filesystem/operations.hpp" // for exists +#include "boost/filesystem/path.hpp" // for path +#include "boost/json/object.hpp" // for object +#include "boost/json/value.hpp" // for value +#include "boost/json/value_ref.hpp" // for array, object +#include "caosdb/authentication.h" // for Authenticator, PlainPassw... +#include "caosdb/connection.h" // for ConnectionConfig, Certifi... +#include "caosdb/exceptions.h" // for ConfigurationError +#include "caosdb/utility.h" // for load_json_file + +namespace caosdb::configuration { +using boost::filesystem::exists; +using boost::filesystem::path; +using boost::json::array; +using boost::json::object; +using boost::json::value; +using caosdb::authentication::Authenticator; +using caosdb::authentication::PlainPasswordAuthenticator; +using caosdb::connection::CertificateProvider; +using caosdb::connection::ConnectionConfig; +using caosdb::connection::ConnectionManager; +using caosdb::connection::InsecureConnectionConfig; +using caosdb::connection::PemFileCertificateProvider; +using caosdb::connection::TlsConnectionConfig; +using caosdb::exceptions::ConfigurationError; +using caosdb::utility::load_json_file; + +/** + * Helper class (no state, just member functions) which should only be used by + * the ConfigurationManager to construct Connection instances from the stored + * configuration. + */ +class ConnectionConfigurationHelper { +public: + friend class ConfigurationManager; + +private: + /** + * @param from - a single connection configuration. + */ + inline auto CreateCertificateProvider(const object &from) const + -> std::unique_ptr<CertificateProvider>; + + /** + * @param from - a single connection configuration. + */ + auto CreateAuthenticator(const object &from) const + -> std::unique_ptr<Authenticator>; + + /** + * @param from - a single connection configuration. + */ + auto + CreateConnectionConfiguration(const bool tls, const std::string &host, + const int port, + const CertificateProvider *certificate_provider, + const Authenticator *authenticator) const + -> std::unique_ptr<ConnectionConfig>; + + /** + * @param from - a single connection configuration. + */ + auto IsTls(const object &from) const -> bool; + + /** + * @param from - a single connection configuration. + */ + auto CreateConnectionConfiguration(const object &from) const + -> std::unique_ptr<ConnectionConfig>; +}; + +/** + * Reads the configuration file and keeps the configuration. Singleton. + * + * Currently, this class can only read a single configuration file. No merging + * or overwriting is supported. + */ +class ConfigurationManager { +public: + static ConfigurationManager &GetInstance() { + static ConfigurationManager instance; + return instance; + }; + + /** + * See mReset. + */ + inline static auto Reset() -> void { GetInstance().mReset(); } + + /** + * See mClear. + */ + inline static auto Clear() -> void { GetInstance().mClear(); } + + /** + * See mLoadSingleJSONConfiguration. + */ + inline static auto LoadSingleJSONConfiguration(const path &json_file) + -> void { + GetInstance().mLoadSingleJSONConfiguration(json_file); + } + + /** + * See mGetConnectionConfiguration. + */ + inline static auto GetConnectionConfiguration(const std::string &name) + -> std::unique_ptr<ConnectionConfig> { + return GetInstance().mGetConnectionConfiguration(name); + } + + /** + * Return the ConnectionConfig for the default connection. + */ + inline static auto GetDefaultConnectionConfiguration() + -> std::unique_ptr<ConnectionConfig> { + return GetInstance().mGetConnectionConfiguration( + GetInstance().mGetDefaultConnectionName()); + } + + /** + * See mGetDefaultConnectionName. + */ + inline static auto GetDefaultConnectionName() -> std::string { + return GetInstance().mGetDefaultConnectionName(); + } + + ConfigurationManager(ConfigurationManager const &) = delete; + void operator=(ConfigurationManager const &) = delete; + +private: + value json_configuration; + ConnectionConfigurationHelper connection_configuration_helper; + inline ConfigurationManager() { InitializeDefaults(); }; + + /** + * Initialize this ConfigurationManager with the defaults. + * + * Currently, this means, that the ConfigurationManager attempts to load the + * first existing file from the LIBCAOSDB_CONFIGURATION_FILES_PRECEDENCE list + * of file locations. + */ + auto InitializeDefaults() -> void; + + /** + * Return a json object representing the current configuration. + */ + auto GetConfiguration() const -> const object &; + + /** + * Return the connection configurations. + */ + auto GetConnections() const -> const object &; + + /** + * Return the configuration for the connection with the given name (as a json + * object). + */ + auto GetConnection(const std::string &name) const -> const object &; + + /** + * Reset this ConfigurationManager. + * + * The current configuration is deleted and a new configuration is being + * loaded via InitializeDefaults. + */ + auto mReset() -> void; + + /** + * Clear this ConfigurationManager. + * + * Afterwards, this ConfigurationManager is uninitilized. + * + * In contrast to mReset, this method only deletes the current configuration + * but does not load a new one via InitializeDefaults. + */ + auto mClear() -> void; + + /** + * Load a configuration from a json file. + */ + auto mLoadSingleJSONConfiguration(const path &json_file) -> void; + + /** + * Return the ConnectionConfig for the connection of the given name. + */ + auto mGetConnectionConfiguration(const std::string &name) const + -> std::unique_ptr<ConnectionConfig>; + + /** + * Return the ConnectionConfig for the default connection. + */ + auto mGetDefaultConnectionName() const -> std::string; +}; + +} // namespace caosdb::configuration +#endif diff --git a/include/caosdb/connection.h b/include/caosdb/connection.h index 4e87702..137066d 100644 --- a/include/caosdb/connection.h +++ b/include/caosdb/connection.h @@ -28,8 +28,9 @@ * @brief Configuration and setup of the connection. */ #include <iosfwd> // for ostream +#include <map> // for map #include <memory> // for shared_ptr, unique_ptr -#include <string> // for string +#include <string> // for string, basic_string #include "caosdb/authentication.h" // for Authenticator #include "caosdb/entity/v1alpha1/main.grpc.pb.h" // for EntityTransactionSe... #include "caosdb/info.h" // for VersionInfo @@ -46,26 +47,27 @@ using caosdb::info::v1alpha1::GeneralInfoService; using caosdb::transaction::Transaction; using grpc::ChannelCredentials; -class CertificateificateProvider { +class CertificateProvider { public: [[nodiscard]] auto virtual GetCertificatePem() const -> std::string = 0; + virtual ~CertificateProvider() = default; }; -class PemFileCertificateProvider : public CertificateificateProvider { +class PemFileCertificateProvider : public CertificateProvider { private: - std::string cacert; + std::string certificate_provider; public: explicit PemFileCertificateProvider(const std::string &path); [[nodiscard]] auto GetCertificatePem() const -> std::string override; }; -class PemCertificateProvider : public CertificateificateProvider { +class PemCertificateProvider : public CertificateProvider { private: - std::string cacert; + std::string certificate_provider; public: - explicit PemCertificateProvider(const std::string &cacert); + explicit PemCertificateProvider(const std::string &certificate_provider); [[nodiscard]] auto GetCertificatePem() const -> std::string override; }; @@ -79,6 +81,7 @@ private: public: ConnectionConfig(const std::string &host, int port); + virtual ~ConnectionConfig() = default; friend auto operator<<(std::ostream &out, const ConnectionConfig &config) -> std::ostream &; @@ -103,16 +106,16 @@ public: class TlsConnectionConfig : public ConnectionConfig { private: std::shared_ptr<ChannelCredentials> credentials; - std::string cacert; + std::string certificate_provider; public: TlsConnectionConfig(const std::string &host, int port); TlsConnectionConfig(const std::string &host, int port, const Authenticator &authenticator); TlsConnectionConfig(const std::string &host, int port, - const CertificateificateProvider &cacert); + const CertificateProvider &certificate_provider); TlsConnectionConfig(const std::string &host, int port, - const CertificateificateProvider &cacert, + const CertificateProvider &certificate_provider, const Authenticator &authenticator); [[nodiscard]] auto GetChannelCredentials() const -> std::shared_ptr<ChannelCredentials> override; @@ -134,5 +137,62 @@ public: [[nodiscard]] auto GetVersionInfo() const -> std::unique_ptr<VersionInfo>; [[nodiscard]] auto CreateTransaction() const -> std::unique_ptr<Transaction>; }; + +/** + * Lazily creates and caches reusable connection instances. Singleton. + * + * This class delegates the configuration of new connections to the global + * ConfigurationManager. + * + * A reset of the ConfigurationManager also resets the ConnectionManager. + * + * @brief Lazily creates and caches reusable connection instances. + */ +class ConnectionManager { +private: + mutable std::map<std::string, std::shared_ptr<Connection>> connections; + mutable std::string default_connection_name; + inline ConnectionManager(){}; + + auto mHasConnection(const std::string &name) const -> bool; + + auto mGetConnection(const std::string &name) const + -> const std::shared_ptr<Connection> &; + + auto mGetDefaultConnection() const -> const std::shared_ptr<Connection> &; + + inline auto mReset() -> void { + connections.clear(); + default_connection_name = std::string(); + } + +public: + static ConnectionManager &GetInstance() { + static ConnectionManager instance; + return instance; + }; + + inline static auto HasConnection(const std::string &name) -> bool { + return ConnectionManager::GetInstance().mHasConnection(name); + }; + + inline static auto GetConnection(const std::string &name) + -> const std::shared_ptr<Connection> & { + return ConnectionManager::GetInstance().mGetConnection(name); + }; + + inline static auto GetDefaultConnection() + -> const std::shared_ptr<Connection> & { + return ConnectionManager::GetInstance().mGetDefaultConnection(); + }; + + inline static auto Reset() -> void { + return ConnectionManager::GetInstance().mReset(); + }; + + ConnectionManager(ConnectionManager const &) = delete; + void operator=(ConnectionManager const &) = delete; +}; + } // namespace caosdb::connection #endif diff --git a/include/caosdb/constants.h.in b/include/caosdb/constants.h.in index b13176c..bfec36b 100644 --- a/include/caosdb/constants.h.in +++ b/include/caosdb/constants.h.in @@ -33,6 +33,21 @@ const int COMPATIBLE_SERVER_VERSION_MAJOR = @libcaosdb_COMPATIBLE_SERVER_VERSION const int COMPATIBLE_SERVER_VERSION_MINOR = @libcaosdb_COMPATIBLE_SERVER_VERSION_MINOR@; const int COMPATIBLE_SERVER_VERSION_PATCH = @libcaosdb_COMPATIBLE_SERVER_VERSION_PATCH@; const char* COMPATIBLE_SERVER_VERSION_PRE_RELEASE = "@libcaosdb_COMPATIBLE_SERVER_VERSION_PRE_RELEASE@"; + +/** + * Precedence of configuration files from highest to lowest. + */ +const char* LIBCAOSDB_CONFIGURATION_FILES_PRECEDENCE[] = { + "$CAOSDB_CLIENT_CONFIGURATION", + "caosdb_client.json", + "caosdb-client.json", + ".caosdb_client.json", + ".caosdb-client.json", + "$HOME/caosdb_client.json", + "$HOME/caosdb-client.json", + "$HOME/.caosdb_client.json", + "$HOME/.caosdb-client.json" +}; // clang-format on #ifdef __cplusplus } // namespace caosdb diff --git a/include/caosdb/exceptions.h b/include/caosdb/exceptions.h index 59ff3d4..a0971a1 100644 --- a/include/caosdb/exceptions.h +++ b/include/caosdb/exceptions.h @@ -45,5 +45,23 @@ public: : runtime_error(what_arg) {} }; +/** + * @brief The connection is known to the ConnectionManager under this name. + */ +class UnknownConnectionError : public runtime_error { +public: + explicit UnknownConnectionError(const std::string &what_arg) + : runtime_error(what_arg) {} +}; + +/** + * @brief Exception for errors of the ConnectionManager. + */ +class ConfigurationError : public runtime_error { +public: + explicit ConfigurationError(const std::string &what_arg) + : runtime_error(what_arg) {} +}; + } // namespace caosdb::exceptions #endif diff --git a/include/caosdb/utility.h b/include/caosdb/utility.h index 61e3a31..5d3ac61 100644 --- a/include/caosdb/utility.h +++ b/include/caosdb/utility.h @@ -114,5 +114,11 @@ inline auto load_json_file(const path &json_file) -> value { return parser.release(); } +inline auto get_home_directory() -> const path { + const auto *const home = getenv("HOME"); + // TODO(tf) Add windowsy way of determining the home directory + return home; +} + } // namespace caosdb::utility #endif diff --git a/include/ccaosdb.h b/include/ccaosdb.h index 24f478a..fdd23ba 100644 --- a/include/ccaosdb.h +++ b/include/ccaosdb.h @@ -192,6 +192,22 @@ int caosdb_connection_get_version_info( caosdb_info_version_info *out, const caosdb_connection_connection *connection); +/** + * Get the default connection from the ConnectionManager. + * + * The default connection is to be specified in a configuration file. + */ +int caosdb_connection_connection_manager_get_default_connection( + caosdb_connection_connection *out); + +/** + * Get a named connection from the ConnectionManager. + * + * The named connection is to be specified in a configuration file. + */ +int caosdb_connection_connection_manager_get_connection( + caosdb_connection_connection *out, const char *name); + #ifdef __cplusplus } #endif diff --git a/requirements.txt b/requirements.txt index 9a211b1..2911ae6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +attrs==21.2.0 bottle==0.12.19 certifi==2021.5.30 chardet==4.0.0 @@ -9,6 +10,7 @@ fasteners==0.16.3 future==0.18.2 idna==2.10 Jinja2==2.11.3 +jsonschema==3.2.0 MarkupSafe==2.0.1 node-semver==0.6.1 packaging==20.9 @@ -17,6 +19,7 @@ pluginbase==1.0.1 Pygments==2.9.0 PyJWT==1.7.1 pyparsing==2.4.7 +pyrsistent==0.18.0 python-dateutil==2.8.1 PyYAML==5.4.1 requests==2.25.1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eeabe29..147bddc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,7 @@ set(libcaosdb_SRC ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/authentication.cpp ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/connection.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/configuration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/transaction.cpp ) diff --git a/src/caosdb/configuration.cpp b/src/caosdb/configuration.cpp new file mode 100644 index 0000000..454f6c7 --- /dev/null +++ b/src/caosdb/configuration.cpp @@ -0,0 +1,289 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ +#include "caosdb/configuration.h" +#include <cstdlib> // for getenv +#include <cassert> // for assert +#include <string> // for char_traits, string +#include "boost/iterator/iterator_facade.hpp" // for iterator_facade_base +#include "boost/json/impl/object.hpp" // for object::at, object::begin +#include "boost/json/string.hpp" // for string +#include "boost/json/string_view.hpp" // for string_view +#include "caosdb/authentication.h" // for PlainPasswordAuthentic... +#include "caosdb/connection.h" // for TlsConnectionConfig +#include "caosdb/constants.h" // for LIBCAOSDB_CONFIGURATIO... +#include "caosdb/exceptions.h" // for ConfigurationError + +namespace caosdb::configuration { +using boost::filesystem::exists; +using boost::filesystem::path; +using boost::json::object; +using boost::json::value; +using caosdb::authentication::Authenticator; +using caosdb::authentication::PlainPasswordAuthenticator; +using caosdb::connection::CertificateProvider; +using caosdb::connection::ConnectionConfig; +using caosdb::connection::InsecureConnectionConfig; +using caosdb::connection::PemFileCertificateProvider; +using caosdb::connection::TlsConnectionConfig; +using caosdb::exceptions::ConfigurationError; +using caosdb::utility::get_home_directory; +using caosdb::utility::load_json_file; + +auto ConnectionConfigurationHelper::CreateCertificateProvider( + const object &from) const -> std::unique_ptr<CertificateProvider> { + std::unique_ptr<CertificateProvider> certificate_provider; + if (from.contains("server_certificate_path")) { + const value &path_str = from.at("server_certificate_path"); + assert(path_str.is_string() == true); + certificate_provider = std::make_unique<PemFileCertificateProvider>( + std::string(path_str.as_string().c_str())); + } + return certificate_provider; +}; + +auto ConnectionConfigurationHelper::CreateAuthenticator( + const object &from) const -> std::unique_ptr<Authenticator> { + std::unique_ptr<Authenticator> authenticator; + if (from.contains("authentication")) { + assert(from.at("authentication").is_object()); + auto authentication = from.at("authentication").as_object(); + auto type = std::string("plain"); + if (authentication.contains("type")) { + assert(authentication.at("type").is_string()); + type = std::string(authentication.at("type").as_string().c_str()); + } + if (type == "plain") { + assert(authentication.contains("username")); + auto username = authentication.at("username"); + assert(username.is_string()); + + assert(authentication.contains("password")); + auto password = authentication.at("password"); + assert(password.is_string()); + + authenticator = std::make_unique<PlainPasswordAuthenticator>( + std::string(username.as_string().c_str()), + std::string(password.as_string().c_str())); + } else { + throw ConfigurationError("Unknow authentication type: '" + type + "'."); + } + } + return authenticator; +}; + +auto ConnectionConfigurationHelper::CreateConnectionConfiguration( + const bool tls, const std::string &host, const int port, + const CertificateProvider *certificate_provider, + const Authenticator *authenticator) const + -> std::unique_ptr<ConnectionConfig> { + if (tls) { + if (certificate_provider != nullptr && authenticator != nullptr) { + // authenticated and special certificate + return std::make_unique<TlsConnectionConfig>( + host, port, *certificate_provider, *authenticator); + } else if (certificate_provider != nullptr) { + // unauthenticated, special certificate + return std::make_unique<TlsConnectionConfig>(host, port, + *certificate_provider); + } else if (authenticator != nullptr) { + // authenticated, no special certificate + return std::make_unique<TlsConnectionConfig>(host, port, *authenticator); + } + // unauthenticated, no special certificate + return std::make_unique<TlsConnectionConfig>(host, port); + } else { + return std::make_unique<InsecureConnectionConfig>(host, port); + } +}; + +auto ConnectionConfigurationHelper::IsTls(const object &from) const -> bool { + bool tls = true; + if (from.contains("tls")) { + auto tls_switch = from.at("tls"); + assert(tls_switch.is_bool()); + tls = tls_switch.as_bool(); + } + return tls; +}; + +auto ConnectionConfigurationHelper::CreateConnectionConfiguration( + const object &from) const -> std::unique_ptr<ConnectionConfig> { + assert(from.contains("host")); + const auto &host = from.at("host"); + assert(host.is_string()); + + assert(from.contains("port")); + const auto &port = from.at("port"); + assert(port.is_int64()); + + auto tls = IsTls(from); + + auto certificate_provider = CreateCertificateProvider(from); + + auto authenticator = CreateAuthenticator(from); + + return CreateConnectionConfiguration( + tls, std::string(host.as_string().c_str()), + static_cast<int>(port.as_int64()), certificate_provider.get(), + authenticator.get()); +}; + +auto ConfigurationManager::mReset() -> void { + mClear(); + InitializeDefaults(); +}; + +auto ConfigurationManager::mClear() -> void { + json_configuration = value(nullptr); + ConnectionManager::Reset(); +} + +auto ConfigurationManager::mLoadSingleJSONConfiguration(const path &json_file) + -> void { + if (!json_configuration.is_null()) { + throw ConfigurationError("This CaosDB client has already been configured."); + } + if (!exists(json_file)) { + throw ConfigurationError("Configuration file does not exist."); + } + + json_configuration = load_json_file(json_file); + // TODO(far future) validate against json-schema +}; + +auto ConfigurationManager::mGetConnectionConfiguration( + const std::string &name) const -> std::unique_ptr<ConnectionConfig> { + auto connection_json = GetConnection(name); + return connection_configuration_helper.CreateConnectionConfiguration( + connection_json); +}; + +auto ConfigurationManager::mGetDefaultConnectionName() const -> std::string { + auto connections = GetConnections(); + if (connections.contains("default")) { + auto default_connection = connections.at("default"); + if (default_connection.is_object()) { + // the name is actually "default" + return std::string("default"); + } else { + assert(default_connection.is_string()); + auto default_connection_name = default_connection.as_string(); + // return the string value of connections.default + return std::string(default_connection_name.c_str()); + } + } + if (connections.size() == 1) { + // return the key of the first and only sub-element of connections. + return connections.begin()->key().to_string(); + } + throw ConfigurationError("Could not determine the default connection."); +}; + +auto ConfigurationManager::GetConfiguration() const -> const object & { + if (json_configuration.is_null()) { + throw ConfigurationError("This CaosDB client has not been configured."); + } + assert(json_configuration.is_object()); + return json_configuration.as_object(); +}; + +auto ConfigurationManager::GetConnections() const -> const object & { + const auto &config = GetConfiguration(); + if (!config.contains("connections")) { + throw ConfigurationError( + "This CaosDB client hasn't any configured connections."); + } + const auto &connections_value = config.at("connections"); + if (connections_value.is_null()) { + throw ConfigurationError( + "This CaosDB client hasn't any configured connections."); + } + assert(connections_value.is_object()); + const auto &connections_object = connections_value.as_object(); + if (connections_object.empty()) { + throw ConfigurationError( + "This CaosDB client hasn't any configured connections."); + } + return connections_object; +}; + +auto ConfigurationManager::GetConnection(const std::string &name) const + -> const object & { + const auto &connections = GetConnections(); + if (connections.contains(name)) { + const auto &result_connection = connections.at(name); + assert(result_connection.is_object()); + return result_connection.as_object(); + } + throw ConfigurationError("The connection '" + name + + "' has not been defined."); +}; + +auto ConfigurationManager::InitializeDefaults() -> void { + + // find the configuration file... + std::unique_ptr<path> configuration_file_path; + for (const std::string &configuration_file : + caosdb::LIBCAOSDB_CONFIGURATION_FILES_PRECEDENCE) { + if (configuration_file == "$CAOSDB_CLIENT_CONFIGURATION") { + // user specified a file via the environment variable + const auto *from_env_var = getenv("CAOSDB_CLIENT_CONFIGURATION"); + if (from_env_var != nullptr) { + configuration_file_path = std::make_unique<path>(from_env_var); + if (exists(*configuration_file_path)) { + break; + } else { + configuration_file_path = nullptr; + // TODO(tf) log warning: "Configuration file under + // $CAOSDB_CLIENT_CONFIGURATION does not exist. + } + } + } else { + // check standard locations + configuration_file_path = std::make_unique<path>(); + const path raw(configuration_file); + // resolve home directory + for (auto segment = raw.begin(); segment != raw.end(); ++segment) { + if (segment->string() == "$HOME") { + path expanded_home(get_home_directory()); + *configuration_file_path /= expanded_home; + } else { + *configuration_file_path /= *segment; + } + } + if (exists(*configuration_file_path)) { + break; + } else { + configuration_file_path = nullptr; + } + } + } + + // ... and use the configuration file + if (configuration_file_path != nullptr) { + // TODO(tf): log which file has been used. + mLoadSingleJSONConfiguration(*configuration_file_path); + } else { + // TODO(tf): log warning: no configuration files has been found + } +} + +} // namespace caosdb::configuration diff --git a/src/caosdb/connection.cpp b/src/caosdb/connection.cpp index 5f271a4..376ffbb 100644 --- a/src/caosdb/connection.cpp +++ b/src/caosdb/connection.cpp @@ -28,7 +28,8 @@ #include <stdexcept> // for runtime_error #include <string> // for operator+, char_tr... #include <memory> -#include "caosdb/authentication.h" // for Authenticator +#include "caosdb/authentication.h" // for Authenticator +#include "caosdb/configuration.h" #include "caosdb/exceptions.h" // for AuthenticationError #include "caosdb/info/v1alpha1/main.grpc.pb.h" // for GeneralInfoService #include "caosdb/info/v1alpha1/main.pb.h" // for GetVersionInfoResp... @@ -39,6 +40,7 @@ namespace caosdb::connection { using caosdb::authentication::Authenticator; +using caosdb::configuration::ConfigurationManager; using caosdb::entity::v1alpha1::EntityTransactionService; using caosdb::exceptions::AuthenticationError; using caosdb::exceptions::ConnectionError; @@ -54,19 +56,20 @@ using grpc::SslCredentialsOptions; PemFileCertificateProvider::PemFileCertificateProvider( const std::string &path) { - this->cacert = load_string_file(path); + this->certificate_provider = load_string_file(path); } auto PemFileCertificateProvider::GetCertificatePem() const -> std::string { - return this->cacert; + return this->certificate_provider; } -PemCertificateProvider::PemCertificateProvider(const std::string &cacert) { - this->cacert = cacert; +PemCertificateProvider::PemCertificateProvider( + const std::string &certificate_provider) { + this->certificate_provider = certificate_provider; } auto PemCertificateProvider::GetCertificatePem() const -> std::string { - return this->cacert; + return this->certificate_provider; } ConnectionConfig::ConnectionConfig(const std::string &host, int port) { @@ -107,10 +110,11 @@ TlsConnectionConfig::TlsConnectionConfig(const std::string &host, int port) } TlsConnectionConfig::TlsConnectionConfig( - const std::string &host, int port, const CertificateificateProvider &cacert) + const std::string &host, int port, + const CertificateProvider &certificate_provider) : ConnectionConfig(host, port) { SslCredentialsOptions options; - options.pem_root_certs = cacert.GetCertificatePem(); + options.pem_root_certs = certificate_provider.GetCertificatePem(); this->credentials = SslCredentials(options); } @@ -124,12 +128,13 @@ TlsConnectionConfig::TlsConnectionConfig(const std::string &host, int port, } TlsConnectionConfig::TlsConnectionConfig( - const std::string &host, int port, const CertificateificateProvider &cacert, + const std::string &host, int port, + const CertificateProvider &certificate_provider, const Authenticator &authenticator) : ConnectionConfig(host, port) { SslCredentialsOptions options; - options.pem_root_certs = cacert.GetCertificatePem(); + options.pem_root_certs = certificate_provider.GetCertificatePem(); this->credentials = grpc::CompositeChannelCredentials( SslCredentials(options), authenticator.GetCallCredentials()); } @@ -141,7 +146,8 @@ auto TlsConnectionConfig::GetChannelCredentials() const auto TlsConnectionConfig::ToString() const -> std::string { return "TlsConnectionConfig(" + this->GetHost() + "," + - std::to_string(this->GetPort()) + "," + this->cacert + ")"; + std::to_string(this->GetPort()) + "," + this->certificate_provider + + ")"; } Connection::Connection(const ConnectionConfig &config) { @@ -188,4 +194,36 @@ auto operator<<(std::ostream &out, const Connection & /*connection*/) return std::make_unique<Transaction>(service_stub); }; +auto ConnectionManager::mHasConnection(const std::string &name) const -> bool { + auto it = connections.find(name); + return it != connections.end(); +} + +auto ConnectionManager::mGetConnection(const std::string &name) const + -> const std::shared_ptr<Connection> & { + if (!HasConnection(name)) { + try { + auto connection = ConfigurationManager::GetConnectionConfiguration(name); + connections[name] = std::make_shared<Connection>(*connection.release()); + } catch (const caosdb::exceptions::ConfigurationError &exc) { + throw caosdb::exceptions::UnknownConnectionError("No connection named '" + + name + "' present."); + } + } + return this->connections.at(name); +} + +auto ConnectionManager::mGetDefaultConnection() const + -> const std::shared_ptr<Connection> & { + if (!HasConnection(default_connection_name)) { + default_connection_name = ConfigurationManager::GetDefaultConnectionName(); + auto default_connection = + ConfigurationManager::GetDefaultConnectionConfiguration(); + connections[default_connection_name] = + std::make_shared<Connection>(*default_connection.release()); + } + + return connections.at(default_connection_name); +} + } // namespace caosdb::connection diff --git a/src/ccaosdb.cpp b/src/ccaosdb.cpp index 412f373..da07682 100644 --- a/src/ccaosdb.cpp +++ b/src/ccaosdb.cpp @@ -1,5 +1,6 @@ #include <iostream> #include <stdio.h> +#include <cassert> #include "caosdb/constants.h" #include "caosdb/utility.h" #include "caosdb/constants.h" @@ -50,7 +51,7 @@ int caosdb_connection_create_pem_file_certificate_provider( int caosdb_connection_delete_certificate_provider( caosdb_connection_certificate_provider *provider) { - delete static_cast<caosdb::connection::CertificateificateProvider *>( + delete static_cast<caosdb::connection::CertificateProvider *>( provider->wrapped_certificate_provider); return 0; } @@ -79,7 +80,7 @@ int caosdb_connection_create_tls_connection_configuration( auto host_str = std::string(host); if (authenticator != nullptr && provider != nullptr) { auto wrapped_provider = - static_cast<caosdb::connection::CertificateificateProvider *>( + static_cast<caosdb::connection::CertificateProvider *>( provider->wrapped_certificate_provider); auto wrapped_authenticator = static_cast<caosdb::authentication::Authenticator *>( @@ -96,7 +97,7 @@ int caosdb_connection_create_tls_connection_configuration( *wrapped_authenticator); } else if (provider != nullptr) { auto wrapped_provider = - static_cast<caosdb::connection::CertificateificateProvider *>( + static_cast<caosdb::connection::CertificateProvider *>( provider->wrapped_certificate_provider); out->wrapped_connection_configuration = new caosdb::connection::TlsConnectionConfig(host_str, port, @@ -165,4 +166,19 @@ int caosdb_connection_get_version_info( return 0; } + +int caosdb_connection_connection_manager_get_default_connection( + caosdb_connection_connection *out) { + out->wrapped_connection = + caosdb::connection::ConnectionManager::GetDefaultConnection().get(); + return 0; +} + +int caosdb_connection_connection_manager_get_connection( + caosdb_connection_connection *out, const char *name) { + out->wrapped_connection = + caosdb::connection::ConnectionManager::GetConnection(std::string(name)) + .get(); + return 0; +} } diff --git a/src/cxxcaosdbcli.cpp b/src/cxxcaosdbcli.cpp index 81fd99c..763ddb5 100644 --- a/src/cxxcaosdbcli.cpp +++ b/src/cxxcaosdbcli.cpp @@ -21,16 +21,14 @@ */ // A simple caosdb client -#include <iostream> -#include <memory> -#include <string> -#include "caosdb/constants.h" -#include "caosdb/connection.h" -#include "caosdb/authentication.h" -#include "caosdb/utility.h" -#include "caosdb/info.h" -#include "caosdb/entity.h" // for Entity, EntityID -#include "caosdb/transaction.h" // for Transaction, UniqueResult +#include <iostream> // for operator<<, basic_ostream, basic_ost... +#include <memory> // for unique_ptr, allocator, __shared_ptr_... +#include <string> // for operator<<, char_traits +#include "caosdb/connection.h" // for Connection, ConnectionManager +#include "caosdb/constants.h" // for LIBCAOSDB_VERSION_MINOR, LIBCAOSDB_V... +#include "caosdb/entity.h" // for Entity +#include "caosdb/info.h" // for VersionInfo +#include "caosdb/transaction.h" // for Transaction, UniqueResult, ResultSet auto main() -> int { std::cout << "CaosDB C++ client (libcaosdb " @@ -40,34 +38,18 @@ auto main() -> int { << "We don't miss the H of caos.\n" << std::endl; - const auto pem_file = - caosdb::utility::get_env_var("CAOSDB_SERVER_CERT", std::string()); - const auto *const host = - caosdb::utility::get_env_var("CAOSDB_SERVER_HOST", "localhost"); - const auto *const port_str = - caosdb::utility::get_env_var("CAOSDB_SERVER_GRPC_PORT_HTTPS", "8443"); - const auto port = std::stoi(port_str); - const auto *const user = caosdb::utility::get_env_var("CAOSDB_USER", "admin"); - const auto *const password = - caosdb::utility::get_env_var("CAOSDB_PASSWORD", "caosdb"); - - // setup the connection - auto auth = - caosdb::authentication::PlainPasswordAuthenticator(user, password); - auto cacert = caosdb::connection::PemFileCertificateProvider(pem_file); - auto config = - caosdb::connection::TlsConnectionConfig(host, port, cacert, auth); - caosdb::connection::Connection connection(config); + const auto &connection = + caosdb::connection::ConnectionManager::GetDefaultConnection(); // get version info of the server - const auto &v_info = connection.GetVersionInfo(); + const auto &v_info = connection->GetVersionInfo(); std::cout << "Server Version: " << v_info->GetMajor() << "." << v_info->GetMinor() << "." << v_info->GetPatch() << "-" << v_info->GetPreRelease() << "-" << v_info->GetBuild() << std::endl; // retrieve an entity - auto transaction(connection.CreateTransaction()); + auto transaction(connection->CreateTransaction()); transaction->RetrieveById("20"); transaction->Execute(); const auto &result_set = diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5d073cd..17613e4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,6 +20,7 @@ # append all the test cases here (file name without the ".cpp" suffix) set(test_cases + test_configuration test_connection test_info test_transaction diff --git a/test/caosdb_test_utility.h.in b/test/caosdb_test_utility.h.in index 8493a89..24d088b 100644 --- a/test/caosdb_test_utility.h.in +++ b/test/caosdb_test_utility.h.in @@ -42,6 +42,18 @@ throw; \ }, \ exeption_type) +#define EXPECT_NULL(statement) \ + if (statement != nullptr) { \ + FAIL() << "Should be a nullptr"; \ + } else { \ + SUCCEED(); \ + } +#define ASSERT_NULL(statement) \ + if (statement != nullptr) { \ + ADD_FAIL() << "Should be a nullptr"; \ + } else { \ + SUCCEED(); \ + } #endif const std::string TEST_DATA_DIR = "@TEST_DATA_DIR@"; diff --git a/test/test_ccaosdb.cpp b/test/test_ccaosdb.cpp index 70f0fb3..b32ace9 100644 --- a/test/test_ccaosdb.cpp +++ b/test/test_ccaosdb.cpp @@ -24,10 +24,40 @@ #include <gtest/gtest-test-part.h> // for SuiteApiResolver, TestFactoryImpl #include <gtest/gtest_pred_impl.h> // for Test, TestInfo, EXPECT_EQ, TEST #include <string> // for allocator +#include "caosdb_test_utility.h" // for EXPECT_THROW_MESSAGE, TEST_DATA_DIR #include "ccaosdb.h" // for caosdb_utility_get_env_var +#include "caosdb/configuration.h" -TEST(test_ccaosdb, test_get_env_var) { +class test_ccaosdb : public ::testing::Test { +protected: + void SetUp() override { + caosdb::configuration::ConfigurationManager::Clear(); + caosdb::configuration::ConfigurationManager::LoadSingleJSONConfiguration( + TEST_DATA_DIR + "/test_caosdb_client.json"); + } + + void TearDown() override { + caosdb::configuration::ConfigurationManager::Clear(); + } +}; + +TEST_F(test_ccaosdb, test_get_env_var) { const char *const some_var = caosdb_utility_get_env_var("SOME_ENV_VAR", "fall-back"); EXPECT_EQ("fall-back", some_var); } + +TEST_F(test_ccaosdb, test_get_default_connection) { + caosdb_connection_connection out; + + caosdb_connection_connection_manager_get_default_connection(&out); + EXPECT_TRUE(out.wrapped_connection); +} + +TEST_F(test_ccaosdb, test_get_connection) { + caosdb_connection_connection out; + + caosdb_connection_connection_manager_get_connection(&out, + "local-caosdb-admin"); + EXPECT_TRUE(out.wrapped_connection); +} diff --git a/test/test_configuration.cpp b/test/test_configuration.cpp new file mode 100644 index 0000000..c7db039 --- /dev/null +++ b/test/test_configuration.cpp @@ -0,0 +1,79 @@ +/* + * + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ +#include <gtest/gtest-message.h> // for Message +#include <gtest/gtest-test-part.h> // for TestPartResult, SuiteApiResolver +#include <gtest/gtest_pred_impl.h> // for Test, TestInfo, TEST +#include <string> // for operator+, allocator, string +#include "caosdb/configuration.h" // for ConfigurationManager, Configurati... +#include "caosdb/exceptions.h" // for ConfigurationError +#include "caosdb_test_utility.h" // for EXPECT_THROW_MESSAGE, TEST_DATA_DIR + +namespace caosdb::configuration { + +class test_configuration : public ::testing::Test { +protected: + void SetUp() override { ConfigurationManager::Clear(); } + void TearDown() override { ConfigurationManager::Clear(); } +}; + +TEST_F(test_configuration, load_json) { + ConfigurationManager::LoadSingleJSONConfiguration(TEST_DATA_DIR + + "/test_caosdb_client.json"); + EXPECT_THROW_MESSAGE( + ConfigurationManager::LoadSingleJSONConfiguration("anything"), + ConfigurationError, "This CaosDB client has already been configured."); + + ConfigurationManager::Clear(); + EXPECT_THROW_MESSAGE( + ConfigurationManager::LoadSingleJSONConfiguration("anything"), + ConfigurationError, "Configuration file does not exist."); + ConfigurationManager::Clear(); +} + +TEST_F(test_configuration, get_default_connection_configuration_error) { + EXPECT_THROW_MESSAGE(ConfigurationManager::GetDefaultConnectionName(), + ConfigurationError, + "This CaosDB client has not been configured."); + + ConfigurationManager::LoadSingleJSONConfiguration( + TEST_DATA_DIR + "/test_broken_caosdb_client_no_connections1.json"); + EXPECT_THROW_MESSAGE(ConfigurationManager::GetDefaultConnectionName(), + ConfigurationError, + "This CaosDB client hasn't any configured connections."); + ConfigurationManager::Clear(); + + ConfigurationManager::LoadSingleJSONConfiguration( + TEST_DATA_DIR + "/test_broken_caosdb_client_no_connections2.json"); + EXPECT_THROW_MESSAGE(ConfigurationManager::GetDefaultConnectionName(), + ConfigurationError, + "This CaosDB client hasn't any configured connections."); + ConfigurationManager::Clear(); + + ConfigurationManager::LoadSingleJSONConfiguration( + TEST_DATA_DIR + "/test_broken_caosdb_client_no_connections3.json"); + EXPECT_THROW_MESSAGE(ConfigurationManager::GetDefaultConnectionName(), + ConfigurationError, + "This CaosDB client hasn't any configured connections."); + ConfigurationManager::Clear(); +} + +} // namespace caosdb::configuration diff --git a/test/test_connection.cpp b/test/test_connection.cpp index 4ea1a7c..b9e967f 100644 --- a/test/test_connection.cpp +++ b/test/test_connection.cpp @@ -21,14 +21,29 @@ */ #include <gtest/gtest-message.h> // for Message -#include <gtest/gtest-test-part.h> // for TestPartResult, SuiteApiRes... -#include <memory> // for allocator, operator!=, shar... -#include "caosdb/connection.h" // for PemCertificateProvider, Insecure... -#include "gtest/gtest_pred_impl.h" // for Test, AssertionResult, EXPE... +#include <gtest/gtest-test-part.h> // for TestPartResult, SuiteApiResolver +#include <memory> // for allocator, operator!=, shared_ptr +#include <string> // for operator+, string +#include "caosdb/configuration.h" // for ConfigurationManager +#include "caosdb/connection.h" // for ConnectionManager, InsecureConnec... +#include "caosdb/exceptions.h" // for UnknownConnectionError +#include "caosdb_test_utility.h" // for EXPECT_THROW_MESSAGE, TEST_DATA_DIR +#include "gtest/gtest_pred_impl.h" // for Test, AssertionResult, TestInfo namespace caosdb::connection { +using caosdb::configuration::ConfigurationManager; -TEST(test_connection, configure_insecure_localhost_8080) { +class test_connection : public ::testing::Test { +protected: + void SetUp() override { + ConfigurationManager::Clear(); + ConfigurationManager::LoadSingleJSONConfiguration( + TEST_DATA_DIR + "/test_caosdb_client.json"); + }; + void TearDown() override { ConfigurationManager::Clear(); }; +}; + +TEST_F(test_connection, configure_insecure_localhost_8080) { InsecureConnectionConfig config("localhost", 8000); EXPECT_EQ("localhost", config.GetHost()); @@ -37,7 +52,7 @@ TEST(test_connection, configure_insecure_localhost_8080) { EXPECT_TRUE(icc != nullptr); } -TEST(test_connection, configure_ssl_localhost_8080) { +TEST_F(test_connection, configure_ssl_localhost_8080) { auto cacert = PemCertificateProvider("ca chain"); TlsConnectionConfig config("localhost", 44300, cacert); @@ -47,4 +62,20 @@ TEST(test_connection, configure_ssl_localhost_8080) { EXPECT_TRUE(sslcc != nullptr); } +TEST_F(test_connection, connection_manager_unknown_connection) { + EXPECT_THROW_MESSAGE(ConnectionManager::GetConnection("test"), + caosdb::exceptions::UnknownConnectionError, + "No connection named 'test' present."); +} + +TEST_F(test_connection, connection_manager_get_default_connection) { + auto connection = ConnectionManager::GetDefaultConnection(); + EXPECT_EQ(connection, ConnectionManager::GetConnection("local-caosdb")); +} + +TEST_F(test_connection, connection_manager_get_connection) { + + EXPECT_TRUE(ConnectionManager::GetConnection("local-caosdb-admin")); +} + } // namespace caosdb::connection diff --git a/test/test_data/test_broken_caosdb_client_no_connections1.json b/test/test_data/test_broken_caosdb_client_no_connections1.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/test/test_data/test_broken_caosdb_client_no_connections1.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/test_data/test_broken_caosdb_client_no_connections2.json b/test/test_data/test_broken_caosdb_client_no_connections2.json new file mode 100644 index 0000000..ab2b5b7 --- /dev/null +++ b/test/test_data/test_broken_caosdb_client_no_connections2.json @@ -0,0 +1,3 @@ +{ + "connections": null +} diff --git a/test/test_data/test_broken_caosdb_client_no_connections3.json b/test/test_data/test_broken_caosdb_client_no_connections3.json new file mode 100644 index 0000000..a4db11b --- /dev/null +++ b/test/test_data/test_broken_caosdb_client_no_connections3.json @@ -0,0 +1,4 @@ +{ + "connections": { + } +} diff --git a/test/test_data/test_caosdb_client.json b/test/test_data/test_caosdb_client.json new file mode 100644 index 0000000..832b0a6 --- /dev/null +++ b/test/test_data/test_caosdb_client.json @@ -0,0 +1,28 @@ +{ + "connections": { + "default": "local-caosdb", + "local-caosdb-admin": { + "host": "localhost", + "port": 8443, + "server_certificate_path": "some/path/cacert.pem", + "authentication": { + "type": "plain", + "username": "admin", + "password": "caosdb" + } + }, + "local-caosdb": { + "host": "localhost", + "port": 8443, + "server_certificate_path": "some/path/cacert.pem", + "authentication": { + "type": "plain", + "username": "me", + "password": "secret!" + } + } + }, + "extension": { + "this is my": "special option" + } +} -- GitLab