diff --git a/API.md b/API.md index 889e5001..0608c90e 100644 --- a/API.md +++ b/API.md @@ -1322,6 +1322,7 @@ If an HTTP status code of 200 is returned, the body of the response will contain { "api_version": "string", "build": "string", + "irods_server_version": "string", // Included for authenticated requests. "irods_zone": "string", "max_number_of_parallel_write_streams": 0, "max_number_of_rows_per_catalog_query": 0, diff --git a/core/include/irods/private/http_api/globals.hpp b/core/include/irods/private/http_api/globals.hpp index 2a07b30f..23d193ab 100644 --- a/core/include/irods/private/http_api/globals.hpp +++ b/core/include/irods/private/http_api/globals.hpp @@ -33,6 +33,9 @@ namespace irods::http::globals auto set_user_mapping_lib(boost::dll::shared_library _lib) -> void; auto user_mapping_lib() -> boost::dll::shared_library&; + + auto set_irods_server_version(std::string _version) -> void; + auto get_irods_server_version() -> const std::string&; } // namespace irods::http::globals #endif // IRODS_HTTP_API_GLOBALS_HPP diff --git a/core/src/globals.cpp b/core/src/globals.cpp index ad752949..cc58f2c6 100644 --- a/core/src/globals.cpp +++ b/core/src/globals.cpp @@ -24,6 +24,9 @@ namespace // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) boost::dll::shared_library g_user_map_lib; + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + std::string g_irods_server_version; } // anonymous namespace namespace irods::http::globals @@ -108,4 +111,14 @@ namespace irods::http::globals { return g_user_map_lib; } // user_mapping_lib + + auto set_irods_server_version(std::string _version) -> void + { + g_irods_server_version = std::move(_version); + } // set_irods_server_version + + auto get_irods_server_version() -> const std::string& + { + return g_irods_server_version; + } // get_irods_server_version } // namespace irods::http::globals diff --git a/core/src/main.cpp b/core/src/main.cpp index 3772e843..a7e4cc9d 100644 --- a/core/src/main.cpp +++ b/core/src/main.cpp @@ -7,6 +7,7 @@ #include "irods/private/http_api/process_stash.hpp" #include "irods/private/http_api/version.hpp" +#include #include #include #include @@ -1075,10 +1076,39 @@ auto main(int _argc, char* _argv[]) -> int std::unique_ptr conn_pool; + // Initialize the connection pool and cache the version of the iRODS server. if (!config.at(json::json_pointer{"/irods_client/enable_4_2_compatibility"}).get()) { logging::trace("Initializing iRODS connection pool."); conn_pool = init_irods_connection_pool(config); irods::http::globals::set_connection_pool(*conn_pool); + + // Cache the version of the connected iRODS server. The /info endpoint includes + // the iRODS server version in the response for authenticated HTTP requests. + // This value MUST NOT influence the behavior of the HTTP API. It is purely for + // the user/application interacting with the HTTP API. + auto conn = conn_pool->get_connection(); + irods::http::globals::set_irods_server_version(static_cast(conn).svrVersion->relVersion); + } + else { + // The admin has decided that no connection pool will be used. Connect to the server + // using the admin credentials. There's no need to authenticate since we're only interested + // in the version of the connected iRODS server. + const auto& host = config.at(json::json_pointer{"/irods_client/host"}).get_ref(); + const auto port = config.at(json::json_pointer{"/irods_client/port"}).get(); + const auto& zone = config.at(json::json_pointer{"/irods_client/zone"}).get_ref(); + const auto& username = config.at(json::json_pointer{"/irods_client/proxy_admin_account/username"}) + .get_ref(); + const irods::experimental::client_connection conn{ + irods::experimental::defer_authentication, + host, + port, + irods::experimental::fully_qualified_username{username, zone}}; + + // Cache the version of the connected iRODS server. The /info endpoint includes + // the iRODS server version in the response for authenticated HTTP requests. + // This value MUST NOT influence the behavior of the HTTP API. It is purely for + // the user/application interacting with the HTTP API. + irods::http::globals::set_irods_server_version(static_cast(conn).svrVersion->relVersion); } // The io_context is required for all I/O. diff --git a/endpoints/information/src/main.cpp b/endpoints/information/src/main.cpp index 9fc21ab6..6f77063e 100644 --- a/endpoints/information/src/main.cpp +++ b/endpoints/information/src/main.cpp @@ -37,7 +37,7 @@ namespace irods::http::handler res.keep_alive(_req.keep_alive()); // clang-format off - res.body() = json{ + json server_info{ {"api_version", irods::http::version::api_version}, {"build", irods::http::version::sha}, {"irods_zone", irods_client_config.at("zone")}, @@ -45,9 +45,15 @@ namespace irods::http::handler {"max_number_of_rows_per_catalog_query", irods_client_config.at("max_number_of_rows_per_catalog_query")}, {"max_size_of_request_body_in_bytes", http_server_config.at(json::json_pointer{"/requests/max_size_of_request_body_in_bytes"})}, {"openid_connect_enabled", http_server_config.contains(json::json_pointer{"/authentication/openid_connect"})} - }.dump(); + }; // clang-format on + // Include the version of the iRODS server if this is an authenticated request. + if (auto result = irods::http::resolve_client_identity(_req); !result.response) { + server_info["irods_server_version"] = globals::get_irods_server_version(); + } + + res.body() = server_info.dump(); res.prepare_payload(); return _sess_ptr->send(std::move(res)); diff --git a/test/test_irods_http_api.py b/test/test_irods_http_api.py index b881c6be..ae1e82c3 100644 --- a/test/test_irods_http_api.py +++ b/test/test_irods_http_api.py @@ -4041,7 +4041,11 @@ class test_information_endpoint(unittest.TestCase): @classmethod def setUpClass(cls): - setup_class(cls, {'endpoint_name': 'info', 'init_rodsadmin': False}) + setup_class(cls, {'endpoint_name': 'info'}) + + @classmethod + def tearDownClass(cls): + tear_down_class(cls) def setUp(self): self.assertFalse(self._class_init_error, 'Class initialization failed. Cannot continue.') @@ -4060,6 +4064,26 @@ def test_expected_properties_exist_in_json_structure(self): self.assertIn('max_size_of_request_body_in_bytes', info) self.assertIn('openid_connect_enabled', info) + # This property should not be available because the request is not from + # an authenticated user. + self.assertNotIn('irods_server_version', info) + + def test_irods_server_version_is_included_in_json_structure_for_authenticated_requests(self): + rodsuser_headers = {'Authorization': f'Bearer {self.rodsuser_bearer_token}'} + r = requests.get(self.url_endpoint, headers=rodsuser_headers) + self.logger.debug(r.content) + self.assertEqual(r.status_code, 200) + + info = r.json() + self.assertIn('api_version', info) + self.assertIn('build', info) + self.assertIn('irods_server_version', info) + self.assertIn('irods_zone', info) + self.assertIn('max_number_of_parallel_write_streams', info) + self.assertIn('max_number_of_rows_per_catalog_query', info) + self.assertIn('max_size_of_request_body_in_bytes', info) + self.assertIn('openid_connect_enabled', info) + def test_server_reports_error_when_http_method_is_not_supported(self): do_test_server_reports_error_when_http_method_is_not_supported(self)