diff --git a/tiledb/CMakeLists.txt b/tiledb/CMakeLists.txt index f8ed77d2059..e736cb489e1 100644 --- a/tiledb/CMakeLists.txt +++ b/tiledb/CMakeLists.txt @@ -4,7 +4,7 @@ # # The MIT License # -# Copyright (c) 2017-2024 TileDB, Inc. +# Copyright (c) 2017-2025 TileDB, Inc. # Copyright (c) 2016 MIT and Intel Corporation # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -133,6 +133,7 @@ endif() set(TILEDB_CORE_SOURCES ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/memory.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/stdx_string.cc + ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/filesystem/home_directory.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/interval/interval.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/common/types/dynamic_typed_datum.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/platform/cert_file.cc @@ -279,6 +280,7 @@ set(TILEDB_CORE_SOURCES ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/query/writers/writer_base.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/query_plan/query_plan.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/rest/rest_client.cc + ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/rest/rest_profile.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/rtree/rtree.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/serialization/array.cc ${TILEDB_CORE_INCLUDE_DIR}/tiledb/sm/serialization/array_directory.cc diff --git a/tiledb/common/CMakeLists.txt b/tiledb/common/CMakeLists.txt index e67cb2b4331..2c4358e6cad 100644 --- a/tiledb/common/CMakeLists.txt +++ b/tiledb/common/CMakeLists.txt @@ -3,7 +3,7 @@ # # The MIT License # -# Copyright (c) 2021-2024 TileDB, Inc. +# Copyright (c) 2021-2025 TileDB, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -43,6 +43,7 @@ add_subdirectory(algorithm) add_subdirectory(dynamic_memory) add_subdirectory(evaluator) add_subdirectory(exception) +add_subdirectory(filesystem) add_subdirectory(governor) add_subdirectory(interval) add_subdirectory(random) diff --git a/tiledb/common/filesystem/CMakeLists.txt b/tiledb/common/filesystem/CMakeLists.txt new file mode 100644 index 00000000000..6bf072ed8a8 --- /dev/null +++ b/tiledb/common/filesystem/CMakeLists.txt @@ -0,0 +1,33 @@ +# +# tiledb/common/filesystem/CMakeLists.txt +# +# The MIT License +# +# Copyright (c) 2025 TileDB, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +include(common NO_POLICY_SCOPE) +include(object_library) + +commence(object_library home_directory) + this_target_sources(home_directory.cc) + this_target_link_libraries(export) + this_target_object_libraries(baseline) +conclude(object_library) diff --git a/tiledb/common/filesystem/home_directory.cc b/tiledb/common/filesystem/home_directory.cc new file mode 100644 index 00000000000..fdbc5b868a8 --- /dev/null +++ b/tiledb/common/filesystem/home_directory.cc @@ -0,0 +1,76 @@ +/** + * @file home_directory.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file implements standalone function home_directory(). + */ + +#ifdef _WIN32 +#include +#include + +#include +#endif + +#include "home_directory.h" +#include "tiledb/common/scoped_executor.h" + +#include +#include + +namespace tiledb::common::filesystem { + +std::string home_directory() { + std::string path = ""; +#ifdef _WIN32 + wchar_t* home; + auto _ = ScopedExecutor([&]() { + if (home) + CoTaskMemFree(home); + }); + if (SHGetKnownFolderPath(FOLDERID_Profile, 0, NULL, &home) == S_OK) { + path = std::wstring_convert>{}.to_bytes(home); + } + // Ensure path has trailing slash. + if (path.back() != '\\') { + path.push_back('\\'); + } +#else + const char* home = std::getenv("HOME"); + if (home != nullptr) { + path = home; + } + // Ensure path has trailing slash. + if (path.back() != '/') { + path.push_back('/'); + } +#endif + return path; +} + +} // namespace tiledb::common::filesystem diff --git a/tiledb/common/filesystem/home_directory.h b/tiledb/common/filesystem/home_directory.h new file mode 100644 index 00000000000..6d9ec602c91 --- /dev/null +++ b/tiledb/common/filesystem/home_directory.h @@ -0,0 +1,50 @@ +/** + * @file home_directory.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines standalone function home_directory(). + */ + +#ifndef TILEDB_HOME_DIRECTORY_H +#define TILEDB_HOME_DIRECTORY_H + +#include + +namespace tiledb::common::filesystem { + +/** + * Standalone function which returns the path to user's home directory. + * + * @invariant `sudo` does not always preserve the path to `$HOME`. Rather than + * throw if the path does not exist, this API will return an empty string. + */ +std::string home_directory(); + +} // namespace tiledb::common::filesystem + +#endif // TILEDB_HOME_DIRECTORY_H diff --git a/tiledb/common/filesystem/test/compile_home_directory_main.cc b/tiledb/common/filesystem/test/compile_home_directory_main.cc new file mode 100644 index 00000000000..0a1b1cb02f9 --- /dev/null +++ b/tiledb/common/filesystem/test/compile_home_directory_main.cc @@ -0,0 +1,36 @@ +/** + * @file compile_home_directory_main.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "../home_directory.h" + +using namespace tiledb::common::filesystem; + +int main() { + (void)home_directory(); + return 0; +} diff --git a/tiledb/sm/misc/constants.cc b/tiledb/sm/misc/constants.cc index 57917dc30c3..07d4f296869 100644 --- a/tiledb/sm/misc/constants.cc +++ b/tiledb/sm/misc/constants.cc @@ -5,7 +5,7 @@ * * The MIT License * - * @copyright Copyright (c) 2017-2024 TileDB, Inc. + * @copyright Copyright (c) 2017-2025 TileDB, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -775,6 +775,15 @@ extern const std::string redirection_header_key = "location"; /** The config key prefix for REST custom headers. */ const std::string rest_header_prefix = "rest.custom_headers."; +/** The current RestProfile API version. */ +const format_version_t rest_profile_version = 1; + +/** Filepath for the special local cloud profile files used in TileDB. */ +const std::string cloud_profile_filepath = ".tiledb/cloud.json"; + +/** Filepath for the special local RestProfile files used in TileDB. */ +const std::string rest_profile_filepath = ".tiledb/profiles.json"; + /** String describing MIME_AUTODETECT. */ const std::string mime_autodetect_str = "AUTODETECT"; diff --git a/tiledb/sm/misc/constants.h b/tiledb/sm/misc/constants.h index 5a854d404b8..f931f68cb4a 100644 --- a/tiledb/sm/misc/constants.h +++ b/tiledb/sm/misc/constants.h @@ -5,7 +5,7 @@ * * The MIT License * - * @copyright Copyright (c) 2017-2024 TileDB, Inc. + * @copyright Copyright (c) 2017-2025 TileDB, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -763,6 +763,15 @@ extern const std::string redirection_header_key; /** The REST custom headers config key prefix. */ extern const std::string rest_header_prefix; +/** The current RestProfile API version. */ +extern const format_version_t rest_profile_version; + +/** Filepath for the special local cloud profile files used in TileDB. */ +extern const std::string cloud_profile_filepath; + +/** Filepath for the special local RestProfile files used in TileDB. */ +extern const std::string rest_profile_filepath; + /** Delimiter for lists passed as config parameter */ extern const std::string config_delimiter; diff --git a/tiledb/sm/rest/CMakeLists.txt b/tiledb/sm/rest/CMakeLists.txt index c830d7c1fda..2f0d544f251 100644 --- a/tiledb/sm/rest/CMakeLists.txt +++ b/tiledb/sm/rest/CMakeLists.txt @@ -3,7 +3,7 @@ # # The MIT License # -# Copyright (c) 2024 TileDB, Inc. +# Copyright (c) 2024-2025 TileDB, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,12 +26,20 @@ include(common NO_POLICY_SCOPE) include(object_library) +# +# Object library `rest_profile` +# +commence(object_library rest_profile) + this_target_sources(rest_profile.cc) + this_target_object_libraries(config home_directory seedable_global_PRNG) +conclude(object_library) + # # Object library `rest_client` # commence(object_library rest_client) this_target_sources(rest_client.cc) - this_target_object_libraries(config) + this_target_object_libraries(rest_profile) conclude(object_library) # @@ -39,4 +47,6 @@ conclude(object_library) # # This object library does not link standalone at present. As long as the # cyclic dependencies in `class RestClient` persist, that won't be possible. -# \ No newline at end of file +# + +add_test_subdirectory() diff --git a/tiledb/sm/rest/rest_profile.cc b/tiledb/sm/rest/rest_profile.cc new file mode 100644 index 00000000000..8d4a776a431 --- /dev/null +++ b/tiledb/sm/rest/rest_profile.cc @@ -0,0 +1,288 @@ +/** + * @file rest_profile.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file implements class RestProfile. + */ + +#include + +#include "rest_profile.h" +#include "tiledb/common/random/random_label.h" +#include "tiledb/sm/misc/constants.h" + +using namespace tiledb::common; +using namespace tiledb::common::filesystem; + +namespace tiledb::sm { + +/* ****************************** */ +/* STATIC API */ +/* ****************************** */ + +/** + * Read the given file and return its contents as a json object. + * + * @param filepath The path of the file to load. + * @return The contents of the file, as a json object. + */ +static json read_file(const std::string& filepath) { + json data; + { + std::ifstream file(filepath); + try { + file >> data; + } catch (...) { + throw RestProfileException("Error parsing file \'" + filepath + "\'."); + } + } + return data; +} + +/** + * Write the given json to the given file. + * + * @param data The json data to write to the file. + * @param filepath The path of the file to which the data is written. + */ +static void write_file(json data, const std::string& filepath) { + // Temporarily append filepath with a random label to guarantee atomicity. + std::string temp_filepath = filepath + "." + random_label(); + + // Load the json object contents into the file. + std::ofstream file(temp_filepath); + try { + file << std::setw(2) << data; + file.flush(); + file.close(); + } catch (...) { + throw RestProfileException( + "Failed to write file due to an internal error during write."); + } + + // Remove the random label from the filepath. + try { + std::filesystem::rename(temp_filepath.c_str(), filepath.c_str()); + } catch (std::filesystem::filesystem_error& e) { + throw RestProfileException( + "Failed to write file due to internal error: " + std::string(e.what())); + } +} + +/* ****************************** */ +/* PARAMETER DEFAULTS */ +/* ****************************** */ + +const std::string RestProfile::DEFAULT_NAME{"default"}; +const std::string RestProfile::DEFAULT_PASSWORD{""}; +const std::string RestProfile::DEFAULT_PAYER_NAMESPACE{""}; +const std::string RestProfile::DEFAULT_TOKEN{""}; +const std::string RestProfile::DEFAULT_SERVER_ADDRESS{"https://api.tiledb.com"}; +const std::string RestProfile::DEFAULT_USERNAME{""}; + +/* ****************************** */ +/* CONSTRUCTORS & DESTRUCTORS */ +/* ****************************** */ + +RestProfile::RestProfile(const std::string& name, const std::string& homedir) + : version_(constants::rest_profile_version) + , name_(name) + , filepath_(homedir + constants::rest_profile_filepath) + , old_filepath_(homedir + constants::cloud_profile_filepath) { + // Fstream cannot create directories. If `homedir/.tiledb/` DNE, create it. + std::filesystem::create_directories(homedir + ".tiledb"); + + // If the local file exists, load the profile with the given name. + if (std::filesystem::exists(filepath_)) { + load_from_json_file(filepath_); + } else { + // If the old version of the file exists, load the profile from there + if (std::filesystem::exists(old_filepath_)) { + load_from_json_file(old_filepath_); + } + } +} + +RestProfile::RestProfile(const std::string& name) { + /** + * Ensure the user's $HOME is found. + * There's an edge case in which `sudo` does not always preserve the path to + * `$HOME`. In this case, the home_directory() API does not throw, but instead + * returns an empty string. As such, we can check for a value in the returned + * path of the home_directory and throw an error to the user accordingly, + * so they may decide the proper course of action: set the $HOME path, + * or perhaps stop using `sudo`. + */ + auto homedir = home_directory(); + if (homedir.empty()) { + throw RestProfileException( + "Failed to create RestProfile; $HOME is not set."); + } + + RestProfile(name, homedir); +} + +/* ****************************** */ +/* API */ +/* ****************************** */ + +void RestProfile::set_param( + const std::string& param, const std::string& value) { + // Validate incoming parameter name + auto it = param_values_.find(param); + if (it == param_values_.end()) { + throw RestProfileException( + "Failed to set parameter of invalid name \'" + param + "\'"); + } + param_values_[param] = value; +} + +std::string RestProfile::get_param(const std::string& param) const { + auto it = param_values_.find(param); + if (it == param_values_.end()) { + throw RestProfileException( + "Failed to retrieve parameter \'" + param + "\'"); + } + return it->second; +} + +/** + * @note The `version_` will always be listed toward the end of the local file. + * `nlohmann::json` does not preserve the structure of the original, top-level + * json object, but rather sorts its elements alphabetically. + * See issue [#727](https://github.com/nlohmann/json/issues/727) for details. + */ +void RestProfile::save_to_file() { + // Validate that the profile is complete (if username is set, so is password) + if ((param_values_["rest.username"] == RestProfile::DEFAULT_USERNAME) != + (param_values_["rest.password"] == RestProfile::DEFAULT_PASSWORD)) { + throw RestProfileException( + "Failed to save; invalid username/password pairing."); + } + + // If the file already exists, load it into a json object. + json data; + if (std::filesystem::exists(filepath_)) { + // Read the file into the json object. + data = read_file(filepath_); + + // If the file is outdated, throw an error. This behavior will evolve. + if (data["version"] < version_) { + throw RestProfileException( + "The version of your local profile.json file is out of date."); + } + + // RestProfiles are immutable, so disallow overwrites. + if (data.contains(name_)) { + throw RestProfileException( + "Failed to save \'" + name_ + + "\'; This profile already exists and " + "must be explicitly removed in order to be replaced."); + } + } else { + // Write the version number iff this is the first time opening the file. + data.push_back(json::object_t::value_type("version", version_)); + } + + // Add this profile to the json object. + data.push_back(json::object_t::value_type(name_, to_json())); + + // Write to the file, which will be created if it does not yet exist. + write_file(data, filepath_); +} + +void RestProfile::remove_from_file() { + if (std::filesystem::exists(filepath_)) { + // Read the file into a json object. + json data = read_file(filepath_); + + // If a profile of the given name exists, remove it. + data.erase(data.find(name_)); + + // Write the json back to the file. + write_file(data, filepath_); + } +} + +json RestProfile::to_json() { + json j; + for (const auto& param : param_values_) { + j[param.first] = param.second; + } + return j; +} + +std::string RestProfile::dump() { + return json{{name_, to_json()}}.dump(2); +} + +/* ****************************** */ +/* PRIVATE API */ +/* ****************************** */ + +void RestProfile::load_from_json_file(const std::string& filename) { + if (filename.empty() || + (filename != filepath_ && filename != old_filepath_)) { + throw RestProfileException( + "Cannot load from \'" + filename + "\'; invalid filename."); + } + + if (!std::filesystem::exists(filename)) { + throw RestProfileException( + "Cannot load from \'" + filename + "\'; file does not exist."); + } + + // Load the file into a json object. + json data = read_file(filename); + + // Update any written-parameters from the loaded json object. + if (filename.c_str() == old_filepath_.c_str()) { + if (data.contains("api_key") && + data["api_key"].contains("X-TILEDB-REST-API-KEY")) { + param_values_["rest.token"] = data["api_key"]["X-TILEDB-REST-API-KEY"]; + } + if (data.contains("host")) { + param_values_["rest.server_address"] = data["host"]; + } + if (data.contains("password")) { + param_values_["rest.password"] = data["password"]; + } + if (data.contains("username")) { + param_values_["rest.username"] = data["username"]; + } + } else { + json profile = data[name_]; + if (!profile.is_null()) { + for (auto it = profile.begin(); it != profile.end(); ++it) { + param_values_[it.key()] = profile[it.key()]; + } + } + } +} + +} // namespace tiledb::sm diff --git a/tiledb/sm/rest/rest_profile.h b/tiledb/sm/rest/rest_profile.h new file mode 100644 index 00000000000..da7d2249056 --- /dev/null +++ b/tiledb/sm/rest/rest_profile.h @@ -0,0 +1,192 @@ +/** + * @file rest_profile.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines class RestProfile. + */ + +#ifndef TILEDB_REST_PROFILE_H +#define TILEDB_REST_PROFILE_H + +#include +#include +#include +#include + +#include "external/include/nlohmann/json.hpp" +#include "tiledb/common/exception/exception.h" +#include "tiledb/common/filesystem/home_directory.h" + +using json = nlohmann::json; +using namespace tiledb::common; + +namespace tiledb::sm { + +class RestProfileException : public StatusException { + public: + explicit RestProfileException(const std::string& message) + : StatusException("RestProfile", message) { + } +}; + +class RestProfile { + /** Make the internals of class RestProfile available to class Config. */ + friend class Config; + + public: + /* ****************************** */ + /* PARAMETER DEFAULTS */ + /* ****************************** */ + + /** The default name of a RestProfile. */ + static const std::string DEFAULT_NAME; + + /** The user's REST password. */ + static const std::string DEFAULT_PASSWORD; + + /** The namespace that should be charged for the request. */ + static const std::string DEFAULT_PAYER_NAMESPACE; + + /** The user's REST token. */ + static const std::string DEFAULT_TOKEN; + + /** The default address for REST server. */ + static const std::string DEFAULT_SERVER_ADDRESS; + + /** The user's REST username. */ + static const std::string DEFAULT_USERNAME; + + /* ********************************* */ + /* CONSTRUCTORS & DESTRUCTORS */ + /* ********************************* */ + + /** + * Constructor. + * + * @param name The name of the RestProfile. Defaulted to "default". + */ + RestProfile(const std::string& name = RestProfile::DEFAULT_NAME); + + /** + * Constructor. Intended for testing purposes only, to preserve the user's + * $HOME path and their profiles from in-test changes. + * + * @param name The name of the RestProfile. + * @param homedir The user's $HOME directory, or desired in-test path. + */ + RestProfile(const std::string& name, const std::string& homedir); + + /** Destructor. */ + ~RestProfile() = default; + + /* ****************************** */ + /* API */ + /* ****************************** */ + + inline std::string name() { + return name_; + } + + /** + * Sets the given parameter to the given value. + * + * @param param The parameter to set. + * @param value The value to set on the given parameter. + */ + void set_param(const std::string& param, const std::string& value); + + /** + * Retrieves the value of the given parameter. + * + * @param param The parameter to fetch. + * @return The value of the given parameter. + */ + std::string get_param(const std::string& param) const; + + /** Saves this profile to the local file. */ + void save_to_file(); + + /** Removes this profile from the local file. */ + void remove_from_file(); + + /** + * Exports this profile's parameters and their values to a json object. + * + * @return A json object of this RestProfile's parameter : value mapping. + */ + json to_json(); + + /** + * Dumps the parameter : value mapping in json object format. + * + * @return This RestProfile's parameter : value mapping in json object format. + */ + std::string dump(); + + private: + /* ********************************* */ + /* PRIVATE API */ + /* ********************************* */ + + /** + * Loads the profile parameters from the given json file, if present. + * + * @param filepath The path of the file to load. + */ + void load_from_json_file(const std::string& filepath); + + /* ********************************* */ + /* PRIVATE ATTRIBUTES */ + /* ********************************* */ + + /** The version of this class. */ + format_version_t version_; + + /** The name of this RestProfile. */ + std::string name_; + + /** The path to the local file which stores all profiles. */ + std::string filepath_; + + /** The path to the old local file which previously stored a profile. */ + std::string old_filepath_; + + /** Stores a map of for the set-parameters. */ + std::map param_values_ = { + std::make_pair("rest.password", RestProfile::DEFAULT_PASSWORD), + std::make_pair( + "rest.payer_namespace", RestProfile::DEFAULT_PAYER_NAMESPACE), + std::make_pair("rest.token", RestProfile::DEFAULT_TOKEN), + std::make_pair( + "rest.server_address", RestProfile::DEFAULT_SERVER_ADDRESS), + std::make_pair("rest.username", RestProfile::DEFAULT_USERNAME)}; +}; + +} // namespace tiledb::sm + +#endif // TILEDB_REST_PROFILE_H diff --git a/tiledb/sm/rest/test/CMakeLists.txt b/tiledb/sm/rest/test/CMakeLists.txt new file mode 100644 index 00000000000..2a6acce5be2 --- /dev/null +++ b/tiledb/sm/rest/test/CMakeLists.txt @@ -0,0 +1,32 @@ +# +# tiledb/sm/rest/test/CMakeLists.txt +# +# The MIT License +# +# Copyright (c) 2025 TileDB, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +include(unit_test) + +commence(unit_test rest_profile) + this_target_sources(main.cc unit_rest_profile.cc) + this_target_link_libraries(rest_profile tiledb_test_support_lib) +conclude(unit_test) diff --git a/tiledb/sm/rest/test/compile_rest_profile_main.cc b/tiledb/sm/rest/test/compile_rest_profile_main.cc new file mode 100644 index 00000000000..c0911156a83 --- /dev/null +++ b/tiledb/sm/rest/test/compile_rest_profile_main.cc @@ -0,0 +1,34 @@ +/** + * @file tiledb/sm/rest/test/compile_rest_profile_main.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "../rest_profile.h" + +int main() { + [[maybe_unused]] tiledb::sm::RestProfile x{}; + return 0; +} diff --git a/tiledb/sm/rest/test/main.cc b/tiledb/sm/rest/test/main.cc new file mode 100644 index 00000000000..21ecba0ab39 --- /dev/null +++ b/tiledb/sm/rest/test/main.cc @@ -0,0 +1,34 @@ +/** + * @file tiledb/sm/rest/test/main.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines a test `main()` + */ + +#define CATCH_CONFIG_MAIN +#include diff --git a/tiledb/sm/rest/test/unit_rest_profile.cc b/tiledb/sm/rest/test/unit_rest_profile.cc new file mode 100644 index 00000000000..a1e381c1637 --- /dev/null +++ b/tiledb/sm/rest/test/unit_rest_profile.cc @@ -0,0 +1,313 @@ +/** + * @file unit_rest_profile.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * Tests the `RestProfile` class. + */ + +#include "test/support/src/temporary_local_directory.h" +#include "tiledb/sm/rest/rest_profile.h" + +#include +#include + +using namespace tiledb::common; +using namespace tiledb::sm; + +std::string cloudtoken_ = "abc123def456"; // Token used by in-test cloud.json. + +/* Tracks the expected name and parameter values for a RestProfile. */ +struct expected_values_t { + std::string name = RestProfile::DEFAULT_NAME; + std::string password = RestProfile::DEFAULT_PASSWORD; + std::string payer_namespace = RestProfile::DEFAULT_PAYER_NAMESPACE; + std::string token = cloudtoken_; + std::string server_address = RestProfile::DEFAULT_SERVER_ADDRESS; + std::string username = RestProfile::DEFAULT_USERNAME; +}; + +struct RestProfileFx { + public: + TemporaryLocalDirectory tempdir_; // A temporary working directory. + std::string homedir_; // The temporary in-test $HOME directory. + std::string cloudpath_; // The in-test path to the cloud.json file. + + RestProfileFx() + : tempdir_(TemporaryLocalDirectory("unit_rest_profile")) + , homedir_(tempdir_.path()) + , cloudpath_(homedir_ + ".tiledb/cloud.json") { + // Fstream cannot create directories, only files, so create the .tiledb dir. + std::filesystem::create_directories(homedir_ + ".tiledb"); + + // Write to the cloud path. + json j = { + {"api_key", + {json::object_t::value_type("X-TILEDB-REST-API-KEY", cloudtoken_)}}, + {"host", RestProfile::DEFAULT_SERVER_ADDRESS}, + {"username", RestProfile::DEFAULT_USERNAME}, + {"password", RestProfile::DEFAULT_PASSWORD}, + {"verify_ssl", "true"}}; + + { + std::ofstream file(cloudpath_); + file << std::setw(2) << j << std::endl; + } + } + + ~RestProfileFx() = default; + + /** Returns a new RestProfile with the given name, at the in-test $HOME. */ + RestProfile create_profile( + const std::string& name = RestProfile::DEFAULT_NAME) { + return RestProfile(name, homedir_); + } + + /** Returns true iff the profile's parameter values match the expected. */ + bool is_valid(RestProfile p, expected_values_t e) { + if (p.name() == e.name && p.get_param("rest.password") == e.password && + p.get_param("rest.payer_namespace") == e.payer_namespace && + p.get_param("rest.token") == e.token && + p.get_param("rest.server_address") == e.server_address && + p.get_param("rest.username") == e.username) + return true; + return false; + } + + /** + * Returns the RestProfile at the given name from the local file, + * as a json object. + */ + json profile_from_file_to_json(std::string filepath, std::string name) { + json data; + if (std::filesystem::exists(filepath)) { + std::ifstream file(filepath); + file >> data; + file.close(); + return data[name]; + } else { + return data; + } + } +}; + +TEST_CASE_METHOD( + RestProfileFx, "REST Profile: Default profile", "[rest_profile][default]") { + // Remove cloud.json to ensure the RestProfile constructor doesn't inherit it. + std::filesystem::remove(cloudpath_); + CHECK(!std::filesystem::exists(cloudpath_)); + + // Create and validate a default RestProfile. + RestProfile profile(create_profile()); + profile.save_to_file(); + + // Set the expected token value; expected_values_t uses cloudtoken_ by + // default. + expected_values_t expected; + expected.token = RestProfile::DEFAULT_TOKEN; + CHECK(is_valid(profile, expected)); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Default profile, empty directory", + "[rest_profile][default][empty_directory]") { + // Remove the .tiledb directory to ensure the cloud.json file isn't inherited. + std::filesystem::remove_all(homedir_ + ".tiledb"); + + // Create and validate a default RestProfile. + RestProfile profile(create_profile()); + profile.save_to_file(); + expected_values_t expected; + expected.token = RestProfile::DEFAULT_TOKEN; + CHECK(is_valid(profile, expected)); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Default profile inherited from cloudpath", + "[rest_profile][default][inherited]") { + // Create and validate a default RestProfile. + RestProfile profile(create_profile()); + profile.save_to_file(); + expected_values_t expected; + CHECK(is_valid(profile, expected)); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Save/Remove", + "[rest_profile][save][remove]") { + // Create a default RestProfile. + RestProfile p(create_profile()); + std::string filepath = homedir_ + ".tiledb/profiles.json"; + CHECK(profile_from_file_to_json(filepath, p.name()).empty()); + + // Save and validate. + p.save_to_file(); + expected_values_t e; + CHECK(is_valid(p, e)); + CHECK(!profile_from_file_to_json(filepath, p.name()).empty()); + + // Remove the profile and validate that the local json object is removed. + p.remove_from_file(); + CHECK(profile_from_file_to_json(filepath, p.name()).empty()); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Non-default profile", + "[rest_profile][non-default]") { + expected_values_t e{ + "non-default", + "password", + "payer_namespace", + "token", + "server_address", + "username"}; + + // Set and validate non-default parameters. + RestProfile p(create_profile(e.name)); + p.set_param("rest.password", e.password); + p.set_param("rest.payer_namespace", e.payer_namespace); + p.set_param("rest.token", e.token); + p.set_param("rest.server_address", e.server_address); + p.set_param("rest.username", e.username); + p.save_to_file(); + CHECK(is_valid(p, e)); +} + +TEST_CASE_METHOD( + RestProfileFx, "REST Profile: to_json", "[rest_profile][to_json]") { + // Create a default RestProfile. + RestProfile p(create_profile()); + p.save_to_file(); + + // Validate. + expected_values_t e; + json j = p.to_json(); + CHECK(j["rest.password"] == e.password); + CHECK(j["rest.payer_namespace"] == e.payer_namespace); + CHECK(j["rest.token"] == e.token); + CHECK(j["rest.server_address"] == e.server_address); + CHECK(j["rest.username"] == e.username); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Get/Set invalid parameters", + "[rest_profile][get_set_invalid]") { + RestProfile p(create_profile()); + + // Try to get a parameter with an invalid name. + REQUIRE_THROWS_WITH( + p.get_param("username"), + Catch::Matchers::ContainsSubstring("Failed to retrieve parameter")); + + // Try to set a parameter with an invalid name. + REQUIRE_THROWS_WITH( + p.set_param("username", "failed_username"), + Catch::Matchers::ContainsSubstring( + "Failed to set parameter of invalid name")); + + // Set username and try to save without setting password. + p.set_param("rest.username", "username"); + REQUIRE_THROWS_WITH( + p.save_to_file(), + Catch::Matchers::ContainsSubstring("invalid username/password pairing")); + // Set password and save valid profile + p.set_param("rest.password", "password"); + p.save_to_file(); + + // Validate. + expected_values_t e; + e.username = "username"; + e.password = "password"; + CHECK(is_valid(p, e)); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Multiple profiles, same name", + "[rest_profile][multiple][same_name]") { + std::string payer_namespace = "payer_namespace"; + std::string token = "token"; + + // Create and validate a RestProfile with default name. + RestProfile p(create_profile()); + p.set_param("rest.payer_namespace", payer_namespace); + p.save_to_file(); + expected_values_t e; + e.payer_namespace = payer_namespace; + CHECK(is_valid(p, e)); + + // Create a second profile, ensuring the payer_namespace is inherited. + RestProfile p2(create_profile()); + CHECK(p2.get_param("rest.payer_namespace") == payer_namespace); + + // Set a non-default token on the second profile. + p2.set_param("rest.token", token); + REQUIRE_THROWS_WITH( + p2.save_to_file(), + Catch::Matchers::ContainsSubstring("profile already exists")); + p.remove_from_file(); + p2.save_to_file(); + e.token = token; + CHECK(is_valid(p2, e)); + + // Ensure the first profile is now out of date. + CHECK(p.get_param("rest.token") == cloudtoken_); +} + +TEST_CASE_METHOD( + RestProfileFx, + "REST Profile: Multiple profiles, different name", + "[rest_profile][multiple][different_name]") { + std::string name = "non-default"; + std::string payer_namespace = "payer_namespace"; + + // Create and validate a RestProfile with default name. + RestProfile p(create_profile()); + p.set_param("rest.payer_namespace", payer_namespace); + p.save_to_file(); + expected_values_t e; + e.payer_namespace = payer_namespace; + CHECK(is_valid(p, e)); + + // Create a second profile with non-default name and ensure the + // payer_namespace and cloudtoken_ are NOT inherited. + RestProfile p2(create_profile(name)); + CHECK(p2.get_param("rest.payer_namespace") != payer_namespace); + p2.save_to_file(); + e.name = name; + e.payer_namespace = RestProfile::DEFAULT_PAYER_NAMESPACE; + e.token = RestProfile::DEFAULT_TOKEN; + CHECK(is_valid(p2, e)); + + // Ensure the default profile is unchanged. + CHECK(p.name() == RestProfile::DEFAULT_NAME); +}