diff --git a/CMakeLists.txt b/CMakeLists.txt index 49ad9261c7..e67dceed1e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,6 +125,9 @@ if (NOT DEFINED INSTALL_TARGETS) set(BMQ_TARGET_PROMETHEUS_NEEDED NO) set(BMQ_TARGET_IT_NEEDED YES) set(BMQ_TARGET_FUZZTESTS_NEEDED NO) + set(BMQ_TARGET_AUTHNPASS_NEEDED YES) + set(BMQ_TARGET_AUTHNFAIL_NEEDED YES) + set(BMQ_TARGET_AUTHNBASIC_NEEDED YES) else() bbproject_check_install_target("bmqbrkr" installBMQBRKR) bbproject_check_install_target("BMQBRKR_NIGHTLY" installNightly) @@ -148,14 +151,20 @@ else() set(BMQ_TARGET_PROMETHEUS_NEEDED NO) set(BMQ_TARGET_IT_NEEDED NO) set(BMQ_TARGET_FUZZTESTS_NEEDED NO) - - bbproject_check_install_target("bmq" installBMQ) - bbproject_check_install_target("mqb" installMQB) - bbproject_check_install_target("bmqbrkrcfg" installBMQBRKRCFG) - bbproject_check_install_target("bmqtool" installBMQTOOL) - bbproject_check_install_target("bmqstoragetool" installBMQSTORAGETOOL) - bbproject_check_install_target("prometheus" installPROMETHEUS) - bbproject_check_install_target("fuzztests" installFUZZTESTS) + set(BMQ_TARGET_AUTHNPASS_NEEDED NO) + set(BMQ_TARGET_AUTHNFAIL_NEEDED NO) + set(BMQ_TARGET_AUTHNBASIC_NEEDED NO) + + bbproject_check_install_target("bmq" installBMQ) + bbproject_check_install_target("mqb" installMQB) + bbproject_check_install_target("bmqbrkrcfg" installBMQBRKRCFG) + bbproject_check_install_target("bmqtool" installBMQTOOL) + bbproject_check_install_target("bmqstoragetool" installBMQSTORAGETOOL) + bbproject_check_install_target("prometheus" installPROMETHEUS) + bbproject_check_install_target("fuzztests" installFUZZTESTS) + bbproject_check_install_target("authnpass" installAUTHNPASS) + bbproject_check_install_target("authnfail" installAUTHNFAIL) + bbproject_check_install_target("authnbasic" installAUTHNBASIC) if (installBMQ) set(BMQ_TARGET_BMQ_NEEDED YES) @@ -200,6 +209,24 @@ else() set(BMQ_TARGET_MQB_NEEDED YES) set(BMQ_TARGET_FUZZTESTS_NEEDED YES) endif() + + if (installAUTHNPASS) + set(BMQ_TARGET_BMQ_NEEDED YES) + set(BMQ_TARGET_MQB_NEEDED YES) + set(BMQ_TARGET_AUTHNPASS_NEEDED YES) + endif() + + if (installAUTHNFAIL) + set(BMQ_TARGET_BMQ_NEEDED YES) + set(BMQ_TARGET_MQB_NEEDED YES) + set(BMQ_TARGET_AUTHNFAIL_NEEDED YES) + endif() + + if (installAUTHNBASIC) + set(BMQ_TARGET_BMQ_NEEDED YES) + set(BMQ_TARGET_MQB_NEEDED YES) + set(BMQ_TARGET_AUTHNBASIC_NEEDED YES) + endif() endif() find_package(Git) @@ -326,7 +353,7 @@ if(NOT BMQ_TARGET_BMQBRKR_NEEDED) return() endif() -# Install all the headers for mqb + bmq +# Install all the headers for mqb + bmq install(TARGETS bmqbrkr_plugins EXPORT BmqbrkrPluginsTargets FILE_SET HEADERS diff --git a/bin/build-darwin.sh b/bin/build-darwin.sh index 1294e29ac4..b7c7669438 100755 --- a/bin/build-darwin.sh +++ b/bin/build-darwin.sh @@ -165,7 +165,7 @@ CMAKE_OPTIONS=(\ PKG_CONFIG_PATH="${DIR_INSTALL}/lib/pkgconfig:${BREW_PKG_CONFIG_PATH}" \ cmake -B "${DIR_BUILD}/blazingmq" -S "${DIR_ROOT}" "${CMAKE_OPTIONS[@]}" -make -C "${DIR_BUILD}/blazingmq" -j 16 +cmake --build "${DIR_BUILD}/blazingmq" --parallel 16 --target bmqbrkr bmqtool all.it echo broker is here: "${DIR_BUILD}/blazingmq/src/applications/bmqbrkr/bmqbrkr.tsk" echo to run the broker: "${DIR_BUILD}/blazingmq/src/applications/bmqbrkr/run" diff --git a/docker/sanitizers/build_sanitizer.sh b/docker/sanitizers/build_sanitizer.sh index e5eb50d60e..dff6e68a24 100755 --- a/docker/sanitizers/build_sanitizer.sh +++ b/docker/sanitizers/build_sanitizer.sh @@ -71,14 +71,14 @@ apt-get install -qy cmake # Install LLVM wget https://apt.llvm.org/llvm.sh -chmod +x llvm.sh +chmod +x llvm.sh LLVM_VERSION=18 LLVM_TAG="llvmorg-18.1.8" ./llvm.sh ${LLVM_VERSION} all # Create version-agnostic pointers to required LLVM binaries. ln -sf /usr/bin/clang-${LLVM_VERSION} /usr/bin/clang -ln -sf /usr/bin/clang++-${LLVM_VERSION} /usr/bin/clang++ +ln -sf /usr/bin/clang++-${LLVM_VERSION} /usr/bin/clang++ ln -sf /usr/bin/llvm-symbolizer-${LLVM_VERSION} /usr/bin/llvm-symbolizer # Set some initial constants diff --git a/src/groups/mqb/mqba/mqba_adminsession.cpp b/src/groups/mqb/mqba/mqba_adminsession.cpp index 49d71038a6..c82227e2ca 100644 --- a/src/groups/mqb/mqba/mqba_adminsession.cpp +++ b/src/groups/mqb/mqba/mqba_adminsession.cpp @@ -317,6 +317,9 @@ void AdminSession::processEvent(const bmqp::Event& event, { // executed by the *IO* thread + // PRECONDITIONS + BSLS_ASSERT_SAFE(!event.isAuthenticationEvent()); + if (!event.isControlEvent()) { BALL_LOG_ERROR << "#ADMCLIENT_UNEXPECTED_EVENT " << description() << ": Unexpected event type: " << event; diff --git a/src/groups/mqb/mqba/mqba_application.cpp b/src/groups/mqb/mqba/mqba_application.cpp index bde93f7b6a..31f9fdc426 100644 --- a/src/groups/mqb/mqba/mqba_application.cpp +++ b/src/groups/mqb/mqba/mqba_application.cpp @@ -18,11 +18,12 @@ #include // MQB +#include #include #include #include -#include #include +#include #include #include #include @@ -170,6 +171,7 @@ Application::Application(bdlmt::EventScheduler* scheduler, , d_allocatorsStatContext_p(allocatorsStatContext) , d_pluginManager_mp() , d_statController_mp() +, d_authenticationController_mp() , d_configProvider_mp() , d_dispatcher_mp() , d_transportManager_mp() @@ -257,7 +259,8 @@ int Application::start(bsl::ostream& errorDescription) rc_DOMAINMANAGER = -8, rc_TRANSPORTMANAGER_LISTEN = -9, rc_ADMIN_POOL_START_FAILURE = -10, - rc_PLUGINMANAGER = -11 + rc_PLUGINMANAGER = -11, + rc_AUTHENTICATIONCONTROLLER = -12, }; int rc = rc_SUCCESS; @@ -299,6 +302,17 @@ int Application::start(bsl::ostream& errorDescription) return (rc * 100) + rc_STATCONTROLLER; // RETURN } + // Start the AuthenticationController + d_authenticationController_mp.load( + new (*d_allocator_p) mqbauthn::AuthenticationController( + d_pluginManager_mp.get(), + d_allocators.get("AuthenticationController")), + d_allocator_p); + rc = d_authenticationController_mp->start(errorDescription); + if (rc != 0) { + return (rc * 100) + rc_AUTHENTICATIONCONTROLLER; // RETURN + } + // Start the config provider d_configProvider_mp.load(new (*d_allocator_p) ConfigProvider( d_allocators.get("ConfigProvider")), @@ -320,6 +334,13 @@ int Application::start(bsl::ostream& errorDescription) } // Start the transport manager + bslma::ManagedPtr authenticatorMp( + new (*d_allocator_p) Authenticator(d_authenticationController_mp.get(), + &d_blobSpPool, + d_scheduler_p, + d_allocators.get("Authenticator")), + d_allocator_p); + SessionNegotiator* sessionNegotiator = new (*d_allocator_p) SessionNegotiator(&d_bufferFactory, d_dispatcher_mp.get(), @@ -340,17 +361,11 @@ int Application::start(bsl::ostream& errorDescription) bslma::ManagedPtr negotiatorMp(sessionNegotiator, d_allocator_p); - bslma::ManagedPtr - initialConnectionHandlerMp( - new (*d_allocator_p) InitialConnectionHandler( - negotiatorMp, - d_allocators.get("InitialConnectionHandler")), - d_allocator_p); - d_transportManager_mp.load(new (*d_allocator_p) mqbnet::TransportManager( d_scheduler_p, &d_bufferFactory, - initialConnectionHandlerMp, + authenticatorMp, + negotiatorMp, d_statController_mp.get(), d_allocators.get("TransportManager")), d_allocator_p); @@ -521,6 +536,7 @@ void Application::stop() STOP_OBJ(d_domainManager_mp, "DomainManager"); STOP_OBJ(d_dispatcher_mp, "Dispatcher"); STOP_OBJ(d_configProvider_mp, "ConfigProvider"); + STOP_OBJ(d_authenticationController_mp, "AuthenticationController"); STOP_OBJ(d_statController_mp, "StatController"); STOP_OBJ(d_pluginManager_mp, "PluginManager"); @@ -530,6 +546,7 @@ void Application::stop() DESTROY_OBJ(d_transportManager_mp, "TransportManager"); DESTROY_OBJ(d_dispatcher_mp, "Dispatcher"); DESTROY_OBJ(d_configProvider_mp, "ConfigProvider"); + DESTROY_OBJ(d_authenticationController_mp, "AuthenticationController"); DESTROY_OBJ(d_statController_mp, "StatController"); DESTROY_OBJ(d_pluginManager_mp, "PluginManager"); diff --git a/src/groups/mqb/mqba/mqba_application.h b/src/groups/mqb/mqba/mqba_application.h index eca14cd39f..a2baacb22b 100644 --- a/src/groups/mqb/mqba/mqba_application.h +++ b/src/groups/mqb/mqba/mqba_application.h @@ -27,6 +27,7 @@ // MQB #include +#include #include #include #include @@ -99,12 +100,14 @@ class Application { private: // PRIVATE TYPES - typedef bslma::ManagedPtr PluginManagerMp; - typedef bslma::ManagedPtr ClusterCatalogMp; - typedef bslma::ManagedPtr ConfigProviderMp; - typedef bslma::ManagedPtr DispatcherMp; - typedef bslma::ManagedPtr DomainManagerMp; - typedef bslma::ManagedPtr StatControllerMp; + typedef bslma::ManagedPtr PluginManagerMp; + typedef bslma::ManagedPtr ClusterCatalogMp; + typedef bslma::ManagedPtr ConfigProviderMp; + typedef bslma::ManagedPtr DispatcherMp; + typedef bslma::ManagedPtr DomainManagerMp; + typedef bslma::ManagedPtr StatControllerMp; + typedef bslma::ManagedPtr + AuthenticationControllerMp; typedef bslma::ManagedPtr TransportManagerMp; typedef bdlcc::SharedObjectPool< bdlbb::Blob, @@ -146,6 +149,9 @@ class Application { /// Statistics controller component. StatControllerMp d_statController_mp; + /// Authentication controller component. + AuthenticationControllerMp d_authenticationController_mp; + ConfigProviderMp d_configProvider_mp; DispatcherMp d_dispatcher_mp; diff --git a/src/groups/mqb/mqba/mqba_authenticator.cpp b/src/groups/mqb/mqba/mqba_authenticator.cpp new file mode 100644 index 0000000000..560470312e --- /dev/null +++ b/src/groups/mqb/mqba/mqba_authenticator.cpp @@ -0,0 +1,479 @@ +// Copyright 2025 Bloomberg Finance L.P. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// mqba_authenticator.h -*-C++-*- +#include + +#include + +/// Implementation Notes +///==================== +/// The 'Authenticator' class manages both authentication and reauthentication +/// for connections. For incoming connections, it authenticates using the +/// received AuthenticationRequest and responds with an AuthenticationResponse. +/// For outgoing connections, it initiates authentication by sending an +/// AuthenticationRequest and awaits an AuthenticationResponse. Upon +/// successful authentication during initial connection, the negotiation +/// process continues. All authentication operations are performed +/// asynchronously. + +// MQB +#include +#include +#include +#include + +// BMQ +#include +#include +#include +#include +#include +#include +#include + +// BDE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace BloombergLP { +namespace mqba { + +// ------------------- +// class Authenticator +// ------------------- + +int Authenticator::onAuthenticationRequest( + bsl::ostream& errorDescription, + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMsg, + const InitialConnectionContextSp& context) +{ + // executed by one of the *IO* threads + + // PRECONDITIONS + BSLS_ASSERT_SAFE(authenticationMsg.isAuthenticationRequestValue()); + BSLS_ASSERT_SAFE(context->isIncoming()); + + BALL_LOG_DEBUG << "Received authentication message from '" + << context->channel()->peerUri(); + + // Create an AuthenticationContext for that connection + bsl::shared_ptr authenticationContext = + bsl::allocate_shared( + d_allocator_p, + context.get(), // initialConnectionContext + authenticationMsg, // authenticationMessage + context + ->authenticationEncodingType(), // authenticationEncodingType + mqbnet::AuthenticationState::e_AUTHENTICATING // state + ); + + context->setAuthenticationContext(authenticationContext); + + // Authenticate + int rc = authenticateAsync( + errorDescription, + authenticationContext, + context->channel(), + context->state() == InitialConnectionState::e_ANON_AUTHENTICATING, + false); + + return rc; +} + +int Authenticator::onAuthenticationResponse( + BSLA_UNUSED bsl::ostream& errorDescription, + BSLA_UNUSED const bmqp_ctrlmsg::AuthenticationMessage& authenticationMsg, + BSLA_UNUSED const InitialConnectionContextSp& context) +{ + // executed by one of the *IO* threads + + BALL_LOG_ERROR << "Not Implemented"; + + return -1; +} + +int Authenticator::sendAuthenticationResponse( + bsl::ostream& errorDescription, + int authnRc, + bsl::string_view errorMsg, + const bsl::optional& lifetimeMs, + const bsl::shared_ptr& channel, + bmqp::EncodingType::Enum authenticationEncodingType) +{ + // executed by an *AUTHENTICATION* thread + enum RcEnum { + // Value for the various RC error categories + rc_SUCCESS = 0, + rc_BUILD_FAILURE = -1, + rc_WRITE_FAILURE = -2 + }; + + // Build authentication response message + bmqp_ctrlmsg::AuthenticationMessage message; + bmqp_ctrlmsg::AuthenticationResponse& response = + message.makeAuthenticationResponse(); + + response.status().code() = authnRc; + response.status().message() = errorMsg; + + if (authnRc != 0) { + response.status().category() = bmqp_ctrlmsg::StatusCategory::E_REFUSED; + } + else { + response.status().category() = bmqp_ctrlmsg::StatusCategory::E_SUCCESS; + response.lifetimeMs() = lifetimeMs; + } + + // Send authentication response message + bdlma::LocalSequentialAllocator<2048> localAllocator(d_allocator_p); + bmqp::SchemaEventBuilder builder(d_blobSpPool_p, + authenticationEncodingType, + &localAllocator); + + int rc = builder.setMessage(response, bmqp::EventType::e_AUTHENTICATION); + if (rc != 0) { + errorDescription << "Failed building AuthenticationMessage " + << "[rc: " << rc << ", message: " << message << "]"; + return rc_BUILD_FAILURE; // RETURN + } + + // Send authnResponse event + bmqio::Status status; + channel->write(&status, *builder.blob()); + if (!status) { + errorDescription << "Failed sending AuthenticationMessage " + << "[status: " << status << ", message: " << message + << "]"; + return rc_WRITE_FAILURE; // RETURN + } + + return rc_SUCCESS; +} + +int Authenticator::authenticateAsync( + bsl::ostream& errorDescription, + const AuthenticationContextSp& context, + const bsl::shared_ptr& channel, + bool isDefaultAuthn, + bool isReauthn) +{ + // executed by one of the *IO* threads + + const int rc = d_threadPool.enqueueJob( + bdlf::BindUtil::bindS(d_allocator_p, + &Authenticator::authenticate, + this, + context, + channel, + isDefaultAuthn, + isReauthn)); + + if (rc != 0) { + errorDescription << "Failed to enqueue authentication job for '" + << channel->peerUri() << "' [rc: " << rc + << ", message: " << context->authenticationMessage() + << "]"; + } + + return rc; +} + +void Authenticator::authenticate( + const AuthenticationContextSp& context, + const bsl::shared_ptr& channel, + bool isDefaultAuthn, + bool isReauthn) +{ + // executed by an *AUTHENTICATION* thread + + // PRECONDITIONS + BSLS_ASSERT(context); + + enum RcEnum { + // Value for the various RC error categories + rc_SUCCESS = 0, + rc_AUTHENTICATION_FAILED = -1, + rc_SCHEDULE_REAUTHN_FAILED = -2, + rc_SEND_AUTHENTICATION_RESPONSE_FAILED = -3, + }; + + int rc = rc_SUCCESS; + bsl::string error; + + // Set up error handler based on whether this is initial authn or reauthn + bsl::optional event; + bsl::optional scopeGuard; + + if (isReauthn) { + // For reauthentication: set up error guard to handle failures + scopeGuard.emplace(bdlf::BindUtil::bind( + &mqbnet::AuthenticationContext::onReauthenticateErrorOrTimeout, + context.get(), + bsl::ref(rc), + "reauthenticationError", + bsl::ref(error), + channel)); + } + else { + // For initial authentication: set up state machine transition + event.emplace(InitialConnectionEvent::e_ERROR); + scopeGuard.emplace(bdlf::BindUtil::bind( + &mqbnet::InitialConnectionContext::handleEvent, + context->initialConnectionContext(), + bsl::ref(error), + bsl::ref(event.value()), + bsl::monostate())); + } + + const bmqp_ctrlmsg::AuthenticationRequest& authenticationRequest = + context->authenticationMessage().authenticationRequest(); + + BALL_LOG_INFO << (isReauthn ? "Reauthenticating" : "Authenticating") + << " connection '" << channel->peerUri() + << "' with mechanism '" << authenticationRequest.mechanism() + << "'"; + + // Authenticate + bmqu::MemOutStream authnErrStream; + bsl::shared_ptr result; + mqbplug::AuthenticationData authenticationData( + authenticationRequest.data().isNull() + ? bsl::vector() + : authenticationRequest.data().value(), + channel->peerUri()); + const int authnRc = d_authnController_p->authenticate( + authnErrStream, + &result, + authenticationRequest.mechanism(), + authenticationData); + + // For anonymous authentication, skip sending the response and proceed + // directly to the next negotiation step + if (isDefaultAuthn) { + BSLS_ASSERT(!isReauthn); + + if (authnRc != 0) { + error = authnErrStream.str(); + } + else { + event.value() = InitialConnectionEvent::e_AUTHN_SUCCESS; + } + return; // RETURN + } + + // Set authentication result, state and schedule reauthentication timer + if (authnRc == 0) { + bmqu::MemOutStream scheduleErrStream; + context->setAuthenticationResult(result); + const int scheduleRc = context->setAuthenticatedAndScheduleReauthn( + scheduleErrStream, + d_scheduler_p, + result->lifetimeMs(), + channel); + if (scheduleRc != 0) { + rc = (scheduleRc * 10) + rc_SCHEDULE_REAUTHN_FAILED; + error = scheduleErrStream.str(); + return; // RETURN + } + } + + // Build authentication response and send it back to the client + bmqu::MemOutStream sendResponseErrStream; + const int sendRc = sendAuthenticationResponse(sendResponseErrStream, + authnRc, + authnErrStream.str(), + result->lifetimeMs(), + channel, + context->encodingType()); + + if (authnRc != 0) { + rc = (authnRc * 10) + rc_AUTHENTICATION_FAILED; + error = authnErrStream.str(); + return; // RETURN + } + + if (sendRc != 0) { + rc = (sendRc * 10) + rc_SEND_AUTHENTICATION_RESPONSE_FAILED; + error = sendResponseErrStream.str(); + return; // RETURN + } + + // Transition to the next state + event.value() = InitialConnectionEvent::e_AUTHN_SUCCESS; +} + +// CREATORS +Authenticator::Authenticator( + mqbauthn::AuthenticationController* authnController, + BlobSpPool* blobSpPool, + bdlmt::EventScheduler* scheduler, + bslma::Allocator* allocator) +: d_allocator_p(allocator) +, d_authnController_p(authnController) +, d_threadPool(bmqsys::ThreadUtil::defaultAttributes(), + mqbcfg::BrokerConfig::get().authentication().minThreads(), + mqbcfg::BrokerConfig::get().authentication().maxThreads(), + bsls::TimeInterval(120).totalMilliseconds(), // idle time + allocator) +, d_blobSpPool_p(blobSpPool) +, d_scheduler_p(scheduler) +, d_isStarted(false) +{ + // PRECONDITIONS + BSLS_ASSERT_SAFE(d_allocator_p); + BSLS_ASSERT_SAFE(d_authnController_p); + BSLS_ASSERT_SAFE(d_blobSpPool_p); + BSLS_ASSERT_SAFE(d_scheduler_p); +} + +/// Destructor +Authenticator::~Authenticator() +{ + // PRECONDITIONS + BSLS_ASSERT_OPT(!d_isStarted && + "stop() must be called before destroying this object"); +} + +int Authenticator::start(bsl::ostream& errorDescription) +{ + if (d_isStarted) { + errorDescription << "start() can only be called once on this object"; + return -1; + } + + BALL_LOG_INFO << "Starting Authenticator"; + + int rc = d_threadPool.start(); + if (rc != 0) { + errorDescription << "Failed to start thread pool for Authenticator" + << "[rc: " << rc << "]"; + return rc; // RETURN + } + + d_isStarted = true; + + return 0; +} + +void Authenticator::stop() +{ + if (!d_isStarted) { + return; // RETURN + } + + d_isStarted = false; + + d_threadPool.stop(); +} + +int Authenticator::handleAuthentication( + bsl::ostream& errorDescription, + const InitialConnectionContextSp& context, + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMsg) +{ + // executed by one of the *IO* threads + + enum RcEnum { + // Value for the various RC error categories + rc_SUCCESS = 0, + rc_HANDLE_MESSAGE_FAIL = -1, + rc_INVALID_MESSAGE = -2, + }; + + int rc = rc_SUCCESS; + + BSLS_ASSERT_SAFE(!context->authenticationContext()); + + switch (authenticationMsg.selectionId()) { + case bmqp_ctrlmsg::AuthenticationMessage:: + SELECTION_ID_AUTHENTICATION_REQUEST: { + rc = onAuthenticationRequest(errorDescription, + authenticationMsg, + context); + } break; // BREAK + case bmqp_ctrlmsg::AuthenticationMessage:: + SELECTION_ID_AUTHENTICATION_RESPONSE: { + rc = onAuthenticationResponse(errorDescription, + authenticationMsg, + context); + } break; // BREAK + default: { + errorDescription + << "Invalid authentication message received (unknown type): " + << authenticationMsg; + return rc_INVALID_MESSAGE; // RETURN + } + } + + if (rc != rc_SUCCESS) { + rc = (rc * 10) + rc_HANDLE_MESSAGE_FAIL; + } + + return rc; +} + +int Authenticator::handleReauthentication( + bsl::ostream& errorDescription, + const AuthenticationContextSp& context, + const bsl::shared_ptr& channel) +{ + // executed by one of the *IO* threads + + enum RcEnum { + // Value for the various RC error categories + rc_SUCCESS = 0, + rc_REAUTHENTICATION_FAILED = -1, + }; + + int rc = + authenticateAsync(errorDescription, context, channel, false, true); + + if (rc != 0) { + rc = (rc * 10) + rc_REAUTHENTICATION_FAILED; + } + + return rc; +} + +int Authenticator::authenticationOutbound( + BSLA_UNUSED bsl::ostream& errorDescription, + BSLA_UNUSED const AuthenticationContextSp& context) +{ + BALL_LOG_ERROR << "Not Implemented"; + + return -1; +} + +// ACCESSORS +const bsl::optional& +Authenticator::anonymousCredential() const +{ + return d_authnController_p->anonymousCredential(); +} + +} // close package namespace +} // close enterprise namespace diff --git a/src/groups/mqb/mqba/mqba_authenticator.h b/src/groups/mqb/mqba/mqba_authenticator.h new file mode 100644 index 0000000000..628afeeaaf --- /dev/null +++ b/src/groups/mqb/mqba/mqba_authenticator.h @@ -0,0 +1,253 @@ +// Copyright 2025 Bloomberg Finance L.P. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// mqba_authenticator.h -*-C++-*- +#ifndef INCLUDED_MQBA_AUTHENTICATOR +#define INCLUDED_MQBA_AUTHENTICATOR + +/// @file mqba_authenticator.h +/// +/// @brief Provide an authenticator for authenticating a connection. +/// +/// @bbref{mqba::Authenticator} implements the @bbref{mqbnet::Authenticator} +/// interface to authenticate a connection with a BlazingMQ client or another +/// bmqbrkr. From a @bbref{bmqio::Channel}, it will exchange authentication +/// message and authenticate depending on the authentication message received. +/// +/// Thread Safety {#mqba_authenticator_thread} +/// ============= +/// This component is *NOT* thread safe. + +// MQB +#include +#include +#include +#include +#include + +// BMQ +#include +#include +#include + +// BDE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace BloombergLP { +namespace mqba { + +// =================== +// class Authenticator +// =================== + +/// Authenticator for a BlazingMQ session with client or broker +class Authenticator : public mqbnet::Authenticator { + private: + // CLASS-SCOPE CATEGORY + BALL_LOG_SET_CLASS_CATEGORY("MQBA.AUTHENTICATOR"); + + public: + // TYPES + + /// Type of a pool of shared pointers to blob + typedef bdlcc::SharedObjectPool< + bdlbb::Blob, + bdlcc::ObjectPoolFunctors::DefaultCreator, + bdlcc::ObjectPoolFunctors::RemoveAll > + BlobSpPool; + + private: + typedef bsl::shared_ptr + AuthenticationContextSp; + + typedef bsl::shared_ptr + InitialConnectionContextSp; + + typedef mqbnet::InitialConnectionEvent InitialConnectionEvent; + typedef mqbnet::InitialConnectionState InitialConnectionState; + + private: + // DATA + + /// Allocator to use. + bslma::Allocator* d_allocator_p; + + /// Authentication Controller. + mqbauthn::AuthenticationController* d_authnController_p; + + /// Thread pool to run authentication and reauthentication tasks. + bdlmt::ThreadPool d_threadPool; + + /// Pool of shared pointers to blobs. Held, not owned. + BlobSpPool* d_blobSpPool_p; + + /// Used to track the duration of a valid authenticated connection. + /// If reauthentication does not occur within the specified time, + /// an event is triggered to close the channel. + bdlmt::EventScheduler* d_scheduler_p; + + /// True if this component is started. + bool d_isStarted; + + private: + // NOT IMPLEMENTED + + /// Copy constructor and assignment operator not implemented. + Authenticator(const Authenticator&); // = delete + Authenticator& operator=(const Authenticator&); // = delete + + private: + // PRIVATE MANIPULATORS + + /// Handle an incoming AuthenticationRequest message by authenticating + /// using the specified `authenticationMsg` and `context`. On success, + /// create an AuthenticationContext and stores it in `context`. The + /// behavior of this function is undefined unless `authenticationMsg` is an + /// `AuthenticationRequest` and this is an incoming connection. + /// Return 0 on success; otherwise, return a non-zero error code and + /// populate `errorDescription` with details of the failure. + int onAuthenticationRequest( + bsl::ostream& errorDescription, + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMsg, + const InitialConnectionContextSp& context); + + /// Handle an incoming AuthenticationResponse message by authenticating + /// using the specified `authenticationMsg` and `context`. On success, + /// create an AuthenticationContext and stores it in `context`. The + /// behavior of this function is undefined unless `authenticationMsg` is an + /// `AuthenticationResponse`. Return 0 on success; otherwise, return a + /// non-zero error code and populate `errorDescription` with details of the + /// failure. + int onAuthenticationResponse( + bsl::ostream& errorDescription, + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMsg, + const InitialConnectionContextSp& context); + + /// Send an authentication response message with the specified `authnRc`, + /// `errorMsg`, and `lifetimeMs` via the specified `channel`, using the + /// specified `authenticationEncodingType`. Return 0 on success; + /// otherwise, return a non-zero error code and populate `errorDescription` + /// with details of the failure. + int sendAuthenticationResponse( + bsl::ostream& errorDescription, + int authnRc, + bsl::string_view errorMsg, + const bsl::optional& lifetimeMs, + const bsl::shared_ptr& channel, + bmqp::EncodingType::Enum authenticationEncodingType); + + /// Schedule an authentication job in the thread pool using the + /// specified `context` and `channel`. The specified `isAnonAuthn` is set + /// to true when this is for anonymous authentication. The specified + /// `isReauthn` is set to true when this is for re-authentication. Return + /// 0 on success, or a non-zero error code and populate the specified + /// `errorDescription` with a description of the error otherwise. + int authenticateAsync(bsl::ostream& errorDescription, + const AuthenticationContextSp& context, + const bsl::shared_ptr& channel, + bool isAnonAuthn, + bool isReauthn); + + // Authenticate the specified `context`. Send an authentication + // response back to the client via the specified `channel` if the specified + // `isDefaultAuthn` is false. If `isReauthn` is true, this is for + // reauthentication, otherwise this is for initial authentication. On + // completion, trigger the initial connection state machine with either + // success or error event. + void authenticate(const AuthenticationContextSp& context, + const bsl::shared_ptr& channel, + bool isDefaultAuthn, + bool isReauthn); + + public: + // TRAITS + BSLMF_NESTED_TRAIT_DECLARATION(Authenticator, bslma::UsesBslmaAllocator) + + public: + // CREATORS + + /// Create a new `Authenticator` using the specified `authnController` and + /// `blobSpPool`. Use the specified `allocator` for all memory + /// allocations. + Authenticator(mqbauthn::AuthenticationController* authnController, + BlobSpPool* blobSpPool, + bdlmt::EventScheduler* scheduler, + bslma::Allocator* allocator); + + /// Destructor + ~Authenticator() BSLS_KEYWORD_OVERRIDE; + + // MANIPULATORS + // (virtual: mqbnet::Authenticator) + + /// Start the authenticator. Return 0 on success, or a non-zero error + /// code and populate the specified `errorDescription` with a description + /// of the error otherwise. + /// This method will block until the thread pool is started. + int start(bsl::ostream& errorDescription) BSLS_KEYWORD_OVERRIDE; + + /// Stop the authenticator. This method will block until the thread pool + /// is stopped. + void stop() BSLS_KEYWORD_OVERRIDE; + + /// Authenticate the connection based on the type of AuthenticationMessage + /// `authenticationMsg`. Create an AuthenticationContext and store into + /// `context`. Return 0 on success, or a non-zero error code and populate + /// the specified `errorDescription` with a description of the error + /// otherwise. + int handleAuthentication(bsl::ostream& errorDescription, + const InitialConnectionContextSp& context, + const bmqp_ctrlmsg::AuthenticationMessage& + authenticationMsg) BSLS_KEYWORD_OVERRIDE; + + /// Handle reauthentication for the specified `context` using the + /// specified `channel`. Return 0 on success, or a non-zero error code and + /// populate the specified `errorDescription` with a description of the + /// error otherwise. + int handleReauthentication(bsl::ostream& errorDescription, + const AuthenticationContextSp& context, + const bsl::shared_ptr& channel) + BSLS_KEYWORD_OVERRIDE; + + /// Send out an outbound authentication message with the specified + /// `context`. Return 0 on success, or a non-zero error code and populate + /// the specified `errorDescription` with a description of the error + /// otherwise. + int authenticationOutbound(bsl::ostream& errorDescription, + const AuthenticationContextSp& context) + BSLS_KEYWORD_OVERRIDE; + + /// ACCESSORS + + /// Return the anonymous credential used for authentication. + /// If no anonymous credential is set, return an empty optional. + const bsl::optional& + anonymousCredential() const BSLS_KEYWORD_OVERRIDE; +}; + +} // close package namespace +} // close enterprise namespace + +#endif diff --git a/src/groups/mqb/mqba/mqba_clientsession.cpp b/src/groups/mqb/mqba/mqba_clientsession.cpp index 90795309da..47f4ff4ab9 100644 --- a/src/groups/mqb/mqba/mqba_clientsession.cpp +++ b/src/groups/mqb/mqba/mqba_clientsession.cpp @@ -2487,6 +2487,9 @@ void ClientSession::processEvent(const bmqp::Event& event, { // executed by the *IO* thread + // PRECONDITIONS + BSLS_ASSERT_SAFE(!event.isAuthenticationEvent()); + if (event.isControlEvent()) { bdlma::LocalSequentialAllocator<2048> localAllocator( d_state.d_allocator_p); diff --git a/src/groups/mqb/mqba/mqba_clientsession.h b/src/groups/mqb/mqba/mqba_clientsession.h index cfe5fec201..61001a2146 100644 --- a/src/groups/mqb/mqba/mqba_clientsession.h +++ b/src/groups/mqb/mqba/mqba_clientsession.h @@ -591,7 +591,8 @@ class ClientSession : public mqbnet::Session, /// Process the specified `event` received from the optionally specified /// `source` node. Note that this method is the entry point for all - /// incoming events coming from the remote peer. + /// incoming events coming from the remote peer. The behavior is undefined + /// unless `event` is not `AuthenticationEvent`. void processEvent(const bmqp::Event& event, mqbnet::ClusterNode* source = 0) BSLS_KEYWORD_OVERRIDE; diff --git a/src/groups/mqb/mqba/mqba_initialconnectionhandler.cpp b/src/groups/mqb/mqba/mqba_initialconnectionhandler.cpp deleted file mode 100644 index 4706f23bfb..0000000000 --- a/src/groups/mqb/mqba/mqba_initialconnectionhandler.cpp +++ /dev/null @@ -1,356 +0,0 @@ -// Copyright 2025 Bloomberg Finance L.P. -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// mqba_initialconnectionhandler.cpp -*-C++-*- -#include - -#include -// MQB -#include -#include - -// BMQ -#include -#include -#include -#include -#include -#include -#include - -// BDE -#include -#include -#include -#include -#include -#include -#include - -namespace BloombergLP { -namespace mqba { - -namespace { - -BALL_LOG_SET_NAMESPACE_CATEGORY("MQBNET.INITIALCONNECTIONHANDLER"); - -const int k_INITIALCONNECTION_READTIMEOUT = 3 * 60; // 3 minutes - -} // close unnamed namespace - -// ------------------------------ -// class InitialConnectionHandler -// ------------------------------ - -void InitialConnectionHandler::readCallback( - const bmqio::Status& status, - int* numNeeded, - bdlbb::Blob* blob, - const InitialConnectionContextSp& context) -{ - enum RcEnum { - // Value for the various RC error categories - rc_SUCCESS = 0, - rc_READ_BLOB_ERROR = -1, - rc_PROCESS_BLOB_ERROR = -2, - }; - - BALL_LOG_TRACE << "InitialConnectionHandler readCb: [status: " << status - << ", peer: '" << context->channel()->peerUri() << "']"; - - bsl::shared_ptr session; - bmqu::MemOutStream errStream; - bdlbb::Blob outPacket; - - bool isFullBlob = true; - int rc = rc_SUCCESS; - bsl::string error; - - // The completeCb is not triggered only when there's more to read - // (didn't receive a full blob; or received a full blob and - // successfully scheduled another read) - bdlb::ScopeExitAny guard( - bdlf::BindUtil::bind(&InitialConnectionHandler::complete, - context, - bsl::ref(rc), - bsl::ref(error), - bsl::ref(session))); - - rc = readBlob(errStream, &outPacket, &isFullBlob, status, numNeeded, blob); - if (rc != rc_SUCCESS) { - rc = (rc * 10) + rc_READ_BLOB_ERROR; - error = bsl::string(errStream.str().data(), errStream.str().length()); - return; // RETURN - } - - if (!isFullBlob) { - guard.release(); - return; // RETURN - } - - rc = processBlob(errStream, &session, outPacket, context); - if (rc != rc_SUCCESS) { - rc = (rc * 10) + rc_PROCESS_BLOB_ERROR; - error = bsl::string(errStream.str().data(), errStream.str().length()); - return; // RETURN - } -} - -int InitialConnectionHandler::readBlob(bsl::ostream& errorDescription, - bdlbb::Blob* outPacket, - bool* isFullBlob, - const bmqio::Status& status, - int* numNeeded, - bdlbb::Blob* blob) -{ - enum RcEnum { - // Value for the various RC error categories - rc_SUCCESS = 0, - rc_READ_ERROR = -1, - rc_UNRECOVERABLE_READ_ERROR = -2 - }; - - if (!status) { - errorDescription << "Read error: " << status; - return (10 * status.category()) + rc_READ_ERROR; // RETURN - } - - int rc = bmqio::ChannelUtil::handleRead(outPacket, numNeeded, blob); - if (rc != 0) { - // This indicates a non recoverable error... - errorDescription << "Unrecoverable read error:\n" - << bmqu::BlobStartHexDumper(blob); - return (rc * 10) + rc_UNRECOVERABLE_READ_ERROR; // RETURN - } - - if (outPacket->length() == 0) { - // Don't yet have a full blob - *isFullBlob = false; - return rc_SUCCESS; // RETURN - } - - // Have a full blob, indicate no more bytes needed (we have to do this - // because 'handleRead' above set it back to 4 at the end). - *numNeeded = 0; - - return rc_SUCCESS; -} - -int InitialConnectionHandler::processBlob( - bsl::ostream& errorDescription, - bsl::shared_ptr* session, - const bdlbb::Blob& blob, - const InitialConnectionContextSp& context) -{ - enum RcEnum { - // Value for the various RC error categories - rc_SUCCESS = 0, - rc_INVALID_NEGOTIATION_MESSAGE = -1, - }; - - bsl::optional negotiationMsg; - - int rc = decodeInitialConnectionMessage(errorDescription, - blob, - &negotiationMsg); - - if (rc != 0) { - return (rc * 10) + rc_INVALID_NEGOTIATION_MESSAGE; // RETURN - } - - if (negotiationMsg.has_value()) { - context->negotiationContext()->setNegotiationMessage( - negotiationMsg.value()); - - rc = d_negotiator_mp->createSessionOnMsgType(errorDescription, - session, - context.get()); - } - else { - errorDescription - << "Decode NegotiationMessage succeeds but nothing is " - "loaded into the NegotiationMessage."; - rc = (rc * 10) + rc_INVALID_NEGOTIATION_MESSAGE; - } - - return rc; -} - -int InitialConnectionHandler::decodeInitialConnectionMessage( - bsl::ostream& errorDescription, - const bdlbb::Blob& blob, - bsl::optional* message) -{ - BSLS_ASSERT(message); - - enum RcEnum { - // Value for the various RC error categories - rc_SUCCESS = 0, - rc_INVALID_MESSAGE = -1, - rc_NOT_CONTROL_EVENT = -2, - rc_INVALID_CONTROL_EVENT = -3 - }; - - bdlma::LocalSequentialAllocator<2048> localAllocator(d_allocator_p); - - bmqp::Event event(&blob, &localAllocator); - - if (!event.isValid()) { - errorDescription << "Invalid negotiation message received " - << "(packet is not a valid BlazingMQ event):\n" - << bmqu::BlobStartHexDumper(&blob); - return rc_INVALID_MESSAGE; // RETURN - } - - if (!event.isControlEvent()) { - errorDescription << "Invalid negotiation message received " - << "(packet is not a ControlEvent):\n" - << bmqu::BlobStartHexDumper(&blob); - return rc_NOT_CONTROL_EVENT; // RETURN - } - - bmqp_ctrlmsg::NegotiationMessage negotiationMessage; - - int rc = event.loadControlEvent(&negotiationMessage); - - if (rc != 0) { - errorDescription << "Invalid negotiation message received (failed " - << "decoding ControlEvent): [rc: " << rc << "]:\n" - << bmqu::BlobStartHexDumper(&blob); - return rc_INVALID_CONTROL_EVENT; // RETURN - } - - *message = negotiationMessage; - - return rc_SUCCESS; -} - -int InitialConnectionHandler::scheduleRead( - bsl::ostream& errorDescription, - const InitialConnectionContextSp& context) -{ - enum RcEnum { - // Value for the various RC error categories - rc_SUCCESS = 0, - rc_READ_ERROR = -1 - }; - - // Schedule a TimedRead - bmqio::Status status; - context->channel()->read( - &status, - bmqp::Protocol::k_PACKET_MIN_SIZE, - bdlf::BindUtil::bind(&InitialConnectionHandler::readCallback, - this, - bdlf::PlaceHolders::_1, // status - bdlf::PlaceHolders::_2, // numNeeded - bdlf::PlaceHolders::_3, // blob - context), - bsls::TimeInterval(k_INITIALCONNECTION_READTIMEOUT)); - // NOTE: In the above binding, we skip '_4' (i.e., Channel*) and - // replace it by the channel shared_ptr (inside the context) - - if (!status) { - errorDescription << "Read failed while negotiating: " << status; - return rc_READ_ERROR; // RETURN - } - - return rc_SUCCESS; -} - -void InitialConnectionHandler::complete( - const InitialConnectionContextSp& context, - const int rc, - const bsl::string& error, - const bsl::shared_ptr& session) -{ - context->complete(rc, error, session); -} - -InitialConnectionHandler::InitialConnectionHandler( - bslma::ManagedPtr& negotiator, - bslma::Allocator* allocator) -: d_negotiator_mp(negotiator) -, d_allocator_p(allocator) -{ -} - -InitialConnectionHandler::~InitialConnectionHandler() -{ -} - -void InitialConnectionHandler::handleInitialConnection( - const InitialConnectionContextSp& context) -{ - // The only counted references to 'InitialConnectionContextSp' are two - // callbacks: - // 1. 'InitialConnectionHandler::complete' which is constructed and - // destructed on stack. - // 2. 'InitialConnectionHandler::readCallback' which the channel holds - // (see 'InitialConnectionHandler::scheduleRead'). - // That means 'InitialConnectionContext' lives as long as there is the need - // to read from the channel. As soon as it sets '*numNeeded = 0', it gets - // destructed after 'InitialConnectionHandler::readCallback' returns. - // If there is a need to keep 'InitialConnectionContext' longer, there - // should be explicit 'bsl::shared_ptr'. - - // Create an NegotiationContext for that connection - bsl::shared_ptr negotiationContext = - bsl::allocate_shared(d_allocator_p, - context.get()); - - context->setNegotiationContext(negotiationContext); - - // Reading for inbound request or continue to read - // after sending a request ourselves - - int rc = 0; - bsl::string error; - - // The completeCb is not triggered only when `scheduleRead` succeeds - // (with or without issuing an outbound message). - bdlb::ScopeExitAny guard( - bdlf::BindUtil::bind(&InitialConnectionHandler::complete, - context, - bsl::ref(rc), - bsl::ref(error), - bsl::shared_ptr())); - - bmqu::MemOutStream errStream; - - if (context->isIncoming()) { - rc = scheduleRead(errStream, context); - } - else { - rc = d_negotiator_mp->negotiateOutbound(errStream, context); - - // Send outbound request success, continue to read - if (rc == 0) { - rc = scheduleRead(errStream, context); - } - } - - if (rc != 0) { - error = bsl::string(errStream.str().data(), errStream.str().length()); - return; - } - - // This line won't be hit. Since if `scheduleRead` succeeds, the same - // callback will be triggered in `readCallback`. - guard.release(); -} - -} // close package namespace -} // close enterprise namespace diff --git a/src/groups/mqb/mqba/mqba_initialconnectionhandler.h b/src/groups/mqb/mqba/mqba_initialconnectionhandler.h deleted file mode 100644 index 8f626a6d99..0000000000 --- a/src/groups/mqb/mqba/mqba_initialconnectionhandler.h +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2025 Bloomberg Finance L.P. -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// mqba_initialconnectionhandler.h -*-C++-*- -#ifndef INCLUDED_MQBA_INITIALCONNECTIONHANDLER -#define INCLUDED_MQBA_INITIALCONNECTIONHANDLER - -#include - -// MQB -#include -#include - -// BMQ -#include -#include -#include - -// BDE -#include -#include -#include -#include -#include - -namespace BloombergLP { - -namespace mqba { - -// FORWARD DECLARATION -class SessionNegotiator; - -// ============================== -// class InitialConnectionHandler -// ============================== - -class InitialConnectionHandler : public mqbnet::InitialConnectionHandler { - private: - // PRIVATE TYPES - typedef bsl::shared_ptr - InitialConnectionContextSp; - - private: - // DATA - - /// Negotiator to use for converting a Channel to a Session - bslma::ManagedPtr d_negotiator_mp; - - /// Allocator to use. - bslma::Allocator* d_allocator_p; - - private: - // NOT IMPLEMENTED - - /// Copy constructor and assignment operator not implemented. - InitialConnectionHandler(const InitialConnectionHandler&); // = delete - InitialConnectionHandler& - operator=(const InitialConnectionHandler&); // = delete - - private: - // PRIVATE MANIPULATORS - - /// Read callback method invoked when receiving data in the specified - /// `blob`, if the specified `status` indicates success. The specified - /// `numNeeded` can be used to indicate if more bytes are needed in - /// order to get a full message. The specified `context` holds the - /// initial connection context associated to this read. - void readCallback(const bmqio::Status& status, - int* numNeeded, - bdlbb::Blob* blob, - const InitialConnectionContextSp& context); - - int readBlob(bsl::ostream& errorDescription, - bdlbb::Blob* outPacket, - bool* isFullBlob, - const bmqio::Status& status, - int* numNeeded, - bdlbb::Blob* blob); - - int processBlob(bsl::ostream& errorDescription, - bsl::shared_ptr* session, - const bdlbb::Blob& blob, - const InitialConnectionContextSp& context); - - /// Decode the initial connection messages received in the specified - /// `blob` and store it, on success, in the specified optional - /// `negotiationMsg`, returning 0. Return a non-zero code on error and - /// populate the specified `errorDescription` with a description of the - /// error. - int decodeInitialConnectionMessage( - bsl::ostream& errorDescription, - const bdlbb::Blob& blob, - bsl::optional* negotiationMsg); - - /// Schedule a read for the initial connection of the session of the - /// specified `context`. Return a non-zero code on error and - /// populate the specified `errorDescription` with a description of the - /// error. - int scheduleRead(bsl::ostream& errorDescription, - const InitialConnectionContextSp& context); - - /// Call the `InitialConnectionCompleteCb` with the specified `context`, - /// return code `rc`, and `error` string to indicate the completion of - /// negotiation. - static void complete(const InitialConnectionContextSp& context, - const int rc, - const bsl::string& error, - const bsl::shared_ptr& session); - - public: - // CREATORS - - InitialConnectionHandler(bslma::ManagedPtr& negotiator, - bslma::Allocator* allocator); - - /// Destructor - ~InitialConnectionHandler() BSLS_KEYWORD_OVERRIDE; - - // MANIPULATORS - - /// Method invoked by the client of this object to negotiate a session. - /// The specified `context` is an in-out member holding the initial - /// connection context to use, including an `InitialConnectionCompleteCb`, - /// which must be called with the result, whether success or failure, of - /// the initial connection. - /// The InitialConnectionHandler concrete implementation can modify some of - /// the members during the initial connection (i.e., between the - /// `handleInitialConnection()` method and the invocation of the - /// `InitialConnectionCompleteCb` method. Note that if no initial - /// connection is needed, the `InitialConnectionCompleteCb` may be invoked - /// directly from inside the call to `handleInitialConnection()`. - void handleInitialConnection(const InitialConnectionContextSp& context) - BSLS_KEYWORD_OVERRIDE; -}; -} -} - -#endif diff --git a/src/groups/mqb/mqba/mqba_sessionnegotiator.cpp b/src/groups/mqb/mqba/mqba_sessionnegotiator.cpp index 3e24c11d35..7fc123588a 100644 --- a/src/groups/mqb/mqba/mqba_sessionnegotiator.cpp +++ b/src/groups/mqb/mqba/mqba_sessionnegotiator.cpp @@ -276,6 +276,10 @@ int SessionNegotiator::createSessionOnMsgType( // - a proxy connecting to us (implying we are a cluster member) // - a cluster peer connecting to us (implying we are a cluster member) // - an admin client connecting to us + + // Authentication needs to be done before we can create a session. + BSLS_ASSERT(context->authenticationContext()); + if (negotiationContext->negotiationMessage() .clientIdentity() .clientType() == bmqp_ctrlmsg::ClientType::E_TCPADMIN) { diff --git a/src/groups/mqb/mqba/mqba_sessionnegotiator.h b/src/groups/mqb/mqba/mqba_sessionnegotiator.h index 1ab8bc5d89..525f15556f 100644 --- a/src/groups/mqb/mqba/mqba_sessionnegotiator.h +++ b/src/groups/mqb/mqba/mqba_sessionnegotiator.h @@ -29,11 +29,12 @@ /// /// Thread Safety {#mqba_sessionnegotiator_thread} /// ============= -/// This component is owned by `InitialConnectionHandler`, and its functions +/// This component is held by `InitialConnectionContext`, and its functions /// are called only from there. It is not thread safe. // MQB #include +#include #include #include #include diff --git a/src/groups/mqb/mqba/package/mqba.dep b/src/groups/mqb/mqba/package/mqba.dep index bf54f30f7c..cd9c6d2191 100644 --- a/src/groups/mqb/mqba/package/mqba.dep +++ b/src/groups/mqb/mqba/package/mqba.dep @@ -1,3 +1,4 @@ +mqbauthn mqbblp mqbcmd mqbnet diff --git a/src/groups/mqb/mqba/package/mqba.mem b/src/groups/mqb/mqba/package/mqba.mem index af31e02095..34216cf910 100644 --- a/src/groups/mqb/mqba/package/mqba.mem +++ b/src/groups/mqb/mqba/package/mqba.mem @@ -1,10 +1,10 @@ mqba_adminsession mqba_application +mqba_authenticator mqba_clientsession mqba_commandrouter mqba_configprovider mqba_dispatcher mqba_domainmanager mqba_domainresolver -mqba_initialconnectionhandler mqba_sessionnegotiator diff --git a/src/groups/mqb/mqbauthn/mqbauthn_authenticationcontroller.cpp b/src/groups/mqb/mqbauthn/mqbauthn_authenticationcontroller.cpp index a99507c1c2..3a88f5b96f 100644 --- a/src/groups/mqb/mqbauthn/mqbauthn_authenticationcontroller.cpp +++ b/src/groups/mqb/mqbauthn/mqbauthn_authenticationcontroller.cpp @@ -460,17 +460,15 @@ int AuthenticationController::authenticate( bmqu::MemOutStream errorStream(d_allocator_p); const int rc = authenticator->authenticate(errorStream, result, input); if (rc != rc_SUCCESS) { - errorDescription << "AuthenticationController: failed to " - "authenticate with mechanism '" + errorDescription << "Failed to authenticate with mechanism '" << normMech << "'. (rc = " << rc << "). Detailed error: " << errorStream.str(); return (rc * 10 + rc_AUTHENTICATION_FAILED); } } else { - errorDescription - << "AuthenticationController: authentication mechanism '" - << normMech << "' not supported."; + errorDescription << "Authentication mechanism '" << normMech + << "' not supported."; return rc_MECHANISM_NOT_SUPPORTED; } diff --git a/src/groups/mqb/mqbblp/mqbblp_cluster.cpp b/src/groups/mqb/mqbblp/mqbblp_cluster.cpp index c0e027e91e..30d3a120ac 100644 --- a/src/groups/mqb/mqbblp/mqbblp_cluster.cpp +++ b/src/groups/mqb/mqbblp/mqbblp_cluster.cpp @@ -3166,6 +3166,11 @@ void Cluster::processEvent(const bmqp::Event& event, // Receipt event arrives from replication nodes to primary. d_storageManager_mp->processReceiptEvent(event, source); } break; // BREAK + case bmqp::EventType::e_AUTHENTICATION: { + // TODO + BALL_LOG_ERROR << "Received Authentication Event but reauthentication " + "logic is not implemented yet."; + } break; // BREAK case bmqp::EventType::e_UNDEFINED: default: { BMQTSK_ALARMLOG_ALARM("CLUSTER") diff --git a/src/groups/mqb/mqbblp/mqbblp_clusterproxy.cpp b/src/groups/mqb/mqbblp/mqbblp_clusterproxy.cpp index 614ab8d81f..080512a277 100644 --- a/src/groups/mqb/mqbblp/mqbblp_clusterproxy.cpp +++ b/src/groups/mqb/mqbblp/mqbblp_clusterproxy.cpp @@ -722,6 +722,11 @@ void ClusterProxy::processEvent(const bmqp::Event& event, .setBlob(blobSp); dispatcher()->dispatchEvent(dispEvent, this); } break; + case bmqp::EventType::e_AUTHENTICATION: { + // TODO + BALL_LOG_ERROR << "Received Authentication Event but reauthentication " + "logic is not implemented yet."; + } break; // BREAK case bmqp::EventType::e_UNDEFINED: case bmqp::EventType::e_CLUSTER_STATE: case bmqp::EventType::e_ELECTOR: diff --git a/src/groups/mqb/mqbcfg/mqbcfg.xsd b/src/groups/mqb/mqbcfg/mqbcfg.xsd index 875e0ace79..7c4f38a14c 100644 --- a/src/groups/mqb/mqbcfg/mqbcfg.xsd +++ b/src/groups/mqb/mqbcfg/mqbcfg.xsd @@ -319,11 +319,17 @@ uses the provided credential with a matching plugin from `authenticators`. When omitted, the broker defaults to AnonAuthenticator and always passes for anonymous authentication. + minThreads..............: + Minimum number of threads in the authentication thread pool. + maxThreads..............: + Maximum number of threads in the authentication thread pool. + + diff --git a/src/groups/mqb/mqbcfg/mqbcfg_messages.cpp b/src/groups/mqb/mqbcfg/mqbcfg_messages.cpp index 8cddd30fc3..63a4193013 100644 --- a/src/groups/mqb/mqbcfg/mqbcfg_messages.cpp +++ b/src/groups/mqb/mqbcfg/mqbcfg_messages.cpp @@ -6008,6 +6008,10 @@ TaskConfig::print(bsl::ostream& stream, int level, int spacesPerLevel) const const char AuthenticatorConfig::CLASS_NAME[] = "AuthenticatorConfig"; +const int AuthenticatorConfig::DEFAULT_INITIALIZER_MIN_THREADS = 1; + +const int AuthenticatorConfig::DEFAULT_INITIALIZER_MAX_THREADS = 3; + const bdlat_AttributeInfo AuthenticatorConfig::ATTRIBUTE_INFO_ARRAY[] = { {ATTRIBUTE_ID_AUTHENTICATORS, "authenticators", @@ -6018,14 +6022,24 @@ const bdlat_AttributeInfo AuthenticatorConfig::ATTRIBUTE_INFO_ARRAY[] = { "anonymousCredential", sizeof("anonymousCredential") - 1, "", - bdlat_FormattingMode::e_DEFAULT}}; + bdlat_FormattingMode::e_DEFAULT}, + {ATTRIBUTE_ID_MIN_THREADS, + "minThreads", + sizeof("minThreads") - 1, + "", + bdlat_FormattingMode::e_DEC}, + {ATTRIBUTE_ID_MAX_THREADS, + "maxThreads", + sizeof("maxThreads") - 1, + "", + bdlat_FormattingMode::e_DEC}}; // CLASS METHODS const bdlat_AttributeInfo* AuthenticatorConfig::lookupAttributeInfo(const char* name, int nameLength) { - for (int i = 0; i < 2; ++i) { + for (int i = 0; i < 4; ++i) { const bdlat_AttributeInfo& attributeInfo = AuthenticatorConfig::ATTRIBUTE_INFO_ARRAY[i]; @@ -6045,6 +6059,10 @@ const bdlat_AttributeInfo* AuthenticatorConfig::lookupAttributeInfo(int id) return &ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_AUTHENTICATORS]; case ATTRIBUTE_ID_ANONYMOUS_CREDENTIAL: return &ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_ANONYMOUS_CREDENTIAL]; + case ATTRIBUTE_ID_MIN_THREADS: + return &ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MIN_THREADS]; + case ATTRIBUTE_ID_MAX_THREADS: + return &ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MAX_THREADS]; default: return 0; } } @@ -6054,6 +6072,8 @@ const bdlat_AttributeInfo* AuthenticatorConfig::lookupAttributeInfo(int id) AuthenticatorConfig::AuthenticatorConfig(bslma::Allocator* basicAllocator) : d_authenticators(basicAllocator) , d_anonymousCredential(basicAllocator) +, d_minThreads(DEFAULT_INITIALIZER_MIN_THREADS) +, d_maxThreads(DEFAULT_INITIALIZER_MAX_THREADS) { } @@ -6061,14 +6081,19 @@ AuthenticatorConfig::AuthenticatorConfig(const AuthenticatorConfig& original, bslma::Allocator* basicAllocator) : d_authenticators(original.d_authenticators, basicAllocator) , d_anonymousCredential(original.d_anonymousCredential, basicAllocator) +, d_minThreads(original.d_minThreads) +, d_maxThreads(original.d_maxThreads) { } #if defined(BSLS_COMPILERFEATURES_SUPPORT_RVALUE_REFERENCES) && \ defined(BSLS_COMPILERFEATURES_SUPPORT_NOEXCEPT) -AuthenticatorConfig::AuthenticatorConfig(AuthenticatorConfig&& original) - noexcept : d_authenticators(bsl::move(original.d_authenticators)), - d_anonymousCredential(bsl::move(original.d_anonymousCredential)) +AuthenticatorConfig::AuthenticatorConfig( + AuthenticatorConfig&& original) noexcept +: d_authenticators(bsl::move(original.d_authenticators)), + d_anonymousCredential(bsl::move(original.d_anonymousCredential)), + d_minThreads(bsl::move(original.d_minThreads)), + d_maxThreads(bsl::move(original.d_maxThreads)) { } @@ -6077,6 +6102,8 @@ AuthenticatorConfig::AuthenticatorConfig(AuthenticatorConfig&& original, : d_authenticators(bsl::move(original.d_authenticators), basicAllocator) , d_anonymousCredential(bsl::move(original.d_anonymousCredential), basicAllocator) +, d_minThreads(bsl::move(original.d_minThreads)) +, d_maxThreads(bsl::move(original.d_maxThreads)) { } #endif @@ -6093,6 +6120,8 @@ AuthenticatorConfig::operator=(const AuthenticatorConfig& rhs) if (this != &rhs) { d_authenticators = rhs.d_authenticators; d_anonymousCredential = rhs.d_anonymousCredential; + d_minThreads = rhs.d_minThreads; + d_maxThreads = rhs.d_maxThreads; } return *this; @@ -6105,6 +6134,8 @@ AuthenticatorConfig& AuthenticatorConfig::operator=(AuthenticatorConfig&& rhs) if (this != &rhs) { d_authenticators = bsl::move(rhs.d_authenticators); d_anonymousCredential = bsl::move(rhs.d_anonymousCredential); + d_minThreads = bsl::move(rhs.d_minThreads); + d_maxThreads = bsl::move(rhs.d_maxThreads); } return *this; @@ -6115,6 +6146,8 @@ void AuthenticatorConfig::reset() { bdlat_ValueTypeFunctions::reset(&d_authenticators); bdlat_ValueTypeFunctions::reset(&d_anonymousCredential); + d_minThreads = DEFAULT_INITIALIZER_MIN_THREADS; + d_maxThreads = DEFAULT_INITIALIZER_MAX_THREADS; } // ACCESSORS @@ -6127,6 +6160,8 @@ bsl::ostream& AuthenticatorConfig::print(bsl::ostream& stream, printer.start(); printer.printAttribute("authenticators", this->authenticators()); printer.printAttribute("anonymousCredential", this->anonymousCredential()); + printer.printAttribute("minThreads", this->minThreads()); + printer.printAttribute("maxThreads", this->maxThreads()); printer.end(); return stream; } diff --git a/src/groups/mqb/mqbcfg/mqbcfg_messages.h b/src/groups/mqb/mqbcfg/mqbcfg_messages.h index ef635d7b61..42c214f9f8 100644 --- a/src/groups/mqb/mqbcfg/mqbcfg_messages.h +++ b/src/groups/mqb/mqbcfg/mqbcfg_messages.h @@ -9052,29 +9052,47 @@ class AuthenticatorConfig { // behavior. When specified, the broker uses the provided credential with // a matching plugin from `authenticators`. When omitted, the broker // defaults to AnonAuthenticator and always passes for anonymous - // authentication. + // authentication. minThreads..............: Minimum number of threads in + // the authentication thread pool. maxThreads..............: Maximum + // number of threads in the authentication thread pool. // INSTANCE DATA bsl::vector d_authenticators; bdlb::NullableValue d_anonymousCredential; + int d_minThreads; + int d_maxThreads; + + // PRIVATE ACCESSORS + template + void hashAppendImpl(t_HASH_ALGORITHM& hashAlgorithm) const; + + bool isEqualTo(const AuthenticatorConfig& rhs) const; public: // TYPES enum { ATTRIBUTE_ID_AUTHENTICATORS = 0, - ATTRIBUTE_ID_ANONYMOUS_CREDENTIAL = 1 + ATTRIBUTE_ID_ANONYMOUS_CREDENTIAL = 1, + ATTRIBUTE_ID_MIN_THREADS = 2, + ATTRIBUTE_ID_MAX_THREADS = 3 }; - enum { NUM_ATTRIBUTES = 2 }; + enum { NUM_ATTRIBUTES = 4 }; enum { ATTRIBUTE_INDEX_AUTHENTICATORS = 0, - ATTRIBUTE_INDEX_ANONYMOUS_CREDENTIAL = 1 + ATTRIBUTE_INDEX_ANONYMOUS_CREDENTIAL = 1, + ATTRIBUTE_INDEX_MIN_THREADS = 2, + ATTRIBUTE_INDEX_MAX_THREADS = 3 }; // CONSTANTS static const char CLASS_NAME[]; + static const int DEFAULT_INITIALIZER_MIN_THREADS; + + static const int DEFAULT_INITIALIZER_MAX_THREADS; + static const bdlat_AttributeInfo ATTRIBUTE_INFO_ARRAY[]; public: @@ -9176,6 +9194,14 @@ class AuthenticatorConfig { // Return a reference to the modifiable "AnonymousCredential" attribute // of this object. + int& minThreads(); + // Return a reference to the modifiable "MinThreads" attribute of this + // object. + + int& maxThreads(); + // Return a reference to the modifiable "MaxThreads" attribute of this + // object. + // ACCESSORS bsl::ostream& print(bsl::ostream& stream, int level = 0, int spacesPerLevel = 4) const; @@ -9228,6 +9254,12 @@ class AuthenticatorConfig { // Return a reference offering non-modifiable access to the // "AnonymousCredential" attribute of this object. + int minThreads() const; + // Return the value of the "MinThreads" attribute of this object. + + int maxThreads() const; + // Return the value of the "MaxThreads" attribute of this object. + // HIDDEN FRIENDS friend bool operator==(const AuthenticatorConfig& lhs, const AuthenticatorConfig& rhs) @@ -9235,8 +9267,7 @@ class AuthenticatorConfig { // have the same value, and 'false' otherwise. Two attribute objects // have the same value if each respective attribute has the same value. { - return lhs.authenticators() == rhs.authenticators() && - lhs.anonymousCredential() == rhs.anonymousCredential(); + return lhs.isEqualTo(rhs); } friend bool operator!=(const AuthenticatorConfig& lhs, @@ -9262,9 +9293,7 @@ class AuthenticatorConfig { // effectively provides a 'bsl::hash' specialization for // 'AuthenticatorConfig'. { - using bslh::hashAppend; - hashAppend(hashAlg, object.authenticators()); - hashAppend(hashAlg, object.anonymousCredential()); + object.hashAppendImpl(hashAlg); } }; @@ -18803,6 +18832,26 @@ inline const LogController& TaskConfig::logController() const // class AuthenticatorConfig // ------------------------- +// PRIVATE ACCESSORS +template +void AuthenticatorConfig::hashAppendImpl(t_HASH_ALGORITHM& hashAlgorithm) const +{ + using bslh::hashAppend; + hashAppend(hashAlgorithm, this->authenticators()); + hashAppend(hashAlgorithm, this->anonymousCredential()); + hashAppend(hashAlgorithm, this->minThreads()); + hashAppend(hashAlgorithm, this->maxThreads()); +} + +inline bool +AuthenticatorConfig::isEqualTo(const AuthenticatorConfig& rhs) const +{ + return this->authenticators() == rhs.authenticators() && + this->anonymousCredential() == rhs.anonymousCredential() && + this->minThreads() == rhs.minThreads() && + this->maxThreads() == rhs.maxThreads(); +} + // CLASS METHODS // MANIPULATORS template @@ -18823,6 +18872,18 @@ int AuthenticatorConfig::manipulateAttributes(t_MANIPULATOR& manipulator) return ret; } + ret = manipulator(&d_minThreads, + ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MIN_THREADS]); + if (ret) { + return ret; + } + + ret = manipulator(&d_maxThreads, + ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MAX_THREADS]); + if (ret) { + return ret; + } + return 0; } @@ -18843,6 +18904,14 @@ int AuthenticatorConfig::manipulateAttribute(t_MANIPULATOR& manipulator, &d_anonymousCredential, ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_ANONYMOUS_CREDENTIAL]); } + case ATTRIBUTE_ID_MIN_THREADS: { + return manipulator(&d_minThreads, + ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MIN_THREADS]); + } + case ATTRIBUTE_ID_MAX_THREADS: { + return manipulator(&d_maxThreads, + ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MAX_THREADS]); + } default: return NOT_FOUND; } } @@ -18875,6 +18944,16 @@ AuthenticatorConfig::anonymousCredential() return d_anonymousCredential; } +inline int& AuthenticatorConfig::minThreads() +{ + return d_minThreads; +} + +inline int& AuthenticatorConfig::maxThreads() +{ + return d_maxThreads; +} + // ACCESSORS template int AuthenticatorConfig::accessAttributes(t_ACCESSOR& accessor) const @@ -18893,6 +18972,18 @@ int AuthenticatorConfig::accessAttributes(t_ACCESSOR& accessor) const return ret; } + ret = accessor(d_minThreads, + ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MIN_THREADS]); + if (ret) { + return ret; + } + + ret = accessor(d_maxThreads, + ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MAX_THREADS]); + if (ret) { + return ret; + } + return 0; } @@ -18911,6 +19002,14 @@ int AuthenticatorConfig::accessAttribute(t_ACCESSOR& accessor, int id) const d_anonymousCredential, ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_ANONYMOUS_CREDENTIAL]); } + case ATTRIBUTE_ID_MIN_THREADS: { + return accessor(d_minThreads, + ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MIN_THREADS]); + } + case ATTRIBUTE_ID_MAX_THREADS: { + return accessor(d_maxThreads, + ATTRIBUTE_INFO_ARRAY[ATTRIBUTE_INDEX_MAX_THREADS]); + } default: return NOT_FOUND; } } @@ -18943,6 +19042,16 @@ AuthenticatorConfig::anonymousCredential() const return d_anonymousCredential; } +inline int AuthenticatorConfig::minThreads() const +{ + return d_minThreads; +} + +inline int AuthenticatorConfig::maxThreads() const +{ + return d_maxThreads; +} + // ----------------------- // class ClusterDefinition // ----------------------- diff --git a/src/groups/mqb/mqbmock/mqbmock_cluster.cpp b/src/groups/mqb/mqbmock/mqbmock_cluster.cpp index d18aa097d7..1f1143f7db 100644 --- a/src/groups/mqb/mqbmock/mqbmock_cluster.cpp +++ b/src/groups/mqb/mqbmock/mqbmock_cluster.cpp @@ -33,6 +33,7 @@ // BDE #include #include +#include #include #include #include @@ -226,10 +227,12 @@ Cluster::Cluster(bslma::Allocator* allocator, , d_isStarted(false) , d_clusterDefinition(allocator) , d_channels(allocator) -, d_initialConnectionHandler_mp() +, d_authenticator_mp() +, d_negotiator_mp() , d_transportManager(&d_scheduler, &d_bufferFactory, - d_initialConnectionHandler_mp, + d_authenticator_mp, + d_negotiator_mp, 0, // mqbstat::StatController* allocator) , d_netCluster_mp(0) diff --git a/src/groups/mqb/mqbmock/mqbmock_cluster.h b/src/groups/mqb/mqbmock/mqbmock_cluster.h index dfff02b9cf..3af7cc1a08 100644 --- a/src/groups/mqb/mqbmock/mqbmock_cluster.h +++ b/src/groups/mqb/mqbmock/mqbmock_cluster.h @@ -101,7 +101,7 @@ class Domain; } namespace mqbnet { class Negotiator; -class InitialConnectionHandler; +class Authenticator; } namespace mqbmock { @@ -120,11 +120,10 @@ class Cluster : public mqbi::Cluster { typedef bsl::function EventProcessor; - typedef bslma::ManagedPtr - InitialConnectionHandlerMp; - typedef bslma::ManagedPtr NegotiatorMp; + typedef bslma::ManagedPtr AuthenticatorMp; + typedef bslma::ManagedPtr NetClusterMp; typedef bslma::ManagedPtr ClusterDataMp; @@ -183,8 +182,11 @@ class Cluster : public mqbi::Cluster { TestChannelMap d_channels; // Test channels - // Initial Connection Handler - InitialConnectionHandlerMp d_initialConnectionHandler_mp; + // Authenticator + AuthenticatorMp d_authenticator_mp; + + // Negotiator + NegotiatorMp d_negotiator_mp; mqbnet::TransportManager d_transportManager; // Transport manager diff --git a/src/groups/mqb/mqbnet/mqbnet_authenticationcontext.cpp b/src/groups/mqb/mqbnet/mqbnet_authenticationcontext.cpp new file mode 100644 index 0000000000..17d830c5a7 --- /dev/null +++ b/src/groups/mqb/mqbnet/mqbnet_authenticationcontext.cpp @@ -0,0 +1,299 @@ +// Copyright 2025 Bloomberg Finance L.P. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// mqbnet_authenticationcontext.cpp -*-C++-*- +#include + +#include +// MQB +#include + +// BMQ +#include +#include +#include +#include +#include + +// BDE +#include +#include +#include +#include +#include +#include + +namespace BloombergLP { +namespace mqbnet { + +// -------------------------- +// struct AuthenticationState +// -------------------------- + +bsl::ostream& AuthenticationState::print(bsl::ostream& stream, + AuthenticationState::Enum value, + int level, + int spacesPerLevel) +{ + if (stream.bad()) { + return stream; // RETURN + } + + bdlb::Print::indent(stream, level, spacesPerLevel); + stream << AuthenticationState::toAscii(value); + + if (spacesPerLevel >= 0) { + stream << '\n'; + } + + return stream; +} + +const char* AuthenticationState::toAscii(AuthenticationState::Enum value) +{ +#define CASE(X) \ + case e_##X: return #X; + + switch (value) { + CASE(AUTHENTICATING) + CASE(AUTHENTICATED) + CASE(CLOSED) + default: return "(* UNKNOWN *)"; + } + +#undef CASE +} + +bool AuthenticationState::fromAscii(AuthenticationState::Enum* out, + const bsl::string_view str) +{ +#define CHECKVALUE(M) \ + if (bdlb::String::areEqualCaseless(toAscii(AuthenticationState::e_##M), \ + str.data(), \ + static_cast(str.length()))) { \ + *out = AuthenticationState::e_##M; \ + return true; \ + } + + CHECKVALUE(AUTHENTICATING) + CHECKVALUE(AUTHENTICATED) + CHECKVALUE(CLOSED) + +#undef CHECKVALUE + return false; +} + +// --------------------------- +// class AuthenticationContext +// --------------------------- + +AuthenticationContext::AuthenticationContext( + InitialConnectionContext* initialConnectionContext, + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMessage, + bmqp::EncodingType::Enum authenticationEncodingType, + AuthenticationState::Enum state, + bslma::Allocator* allocator) +: d_allocator_p(allocator) +, d_self(this) // use default allocator +, d_mutex() +, d_authenticationResultSp() +, d_timeoutHandle() +, d_state(state) +, d_initialConnectionContext_p(initialConnectionContext) +, d_authenticationMessage(authenticationMessage) +, d_encodingType(authenticationEncodingType) +{ + // NOTHING +} + +void AuthenticationContext::setAuthenticationResult( + const bsl::shared_ptr& value) +{ + d_authenticationResultSp = value; +} + +void AuthenticationContext::setAuthenticationMessage( + const bmqp_ctrlmsg::AuthenticationMessage& value) +{ + // PRECONDITION + BSLS_ASSERT_SAFE(d_state == AuthenticationState::e_AUTHENTICATED); + + d_authenticationMessage = value; +} + +void AuthenticationContext::setAuthenticationEncodingType( + bmqp::EncodingType::Enum value) +{ + // PRECONDITION + BSLS_ASSERT_SAFE(d_state == AuthenticationState::e_AUTHENTICATED); + + d_encodingType = value; +} + +void AuthenticationContext::resetAuthenticationMessage() +{ + bslmt::LockGuard guard(&d_mutex); // LOCKED + + d_authenticationMessage.reset(); +} + +int AuthenticationContext::setAuthenticatedAndScheduleReauthn( + bsl::ostream& errorDescription, + bdlmt::EventScheduler* scheduler_p, + const bsl::optional& lifetimeMs, + const bsl::shared_ptr& channel) +{ + // executed by an *AUTHENTICATION* thread + + // PRECONDITION + BSLS_ASSERT_SAFE(scheduler_p); + + bslmt::LockGuard guard(&d_mutex); // LOCKED + + // d_state might be e_CLOSED if the connection is closed and + // AuthenticationContext::onClose() is called before the authentication is + // completed. + if (d_state != AuthenticationState::e_AUTHENTICATING) { + errorDescription << "State not AUTHENTICATING (is " << d_state << ")"; + return -1; + } + + d_state = AuthenticationState::e_AUTHENTICATED; + + if (d_timeoutHandle) { + scheduler_p->cancelEventAndWait(&d_timeoutHandle); + } + + if (lifetimeMs.has_value()) { + bsls::Types::Int64 lifetime = lifetimeMs.value(); + + if (lifetime < 0) { + BALL_LOG_WARN + << "Authenticator returned negative remaining lifetime: " + << bsl::to_string(lifetime) + << ". Schedule reauthentication timer with lifetime set to 0."; + lifetime = 0; + } + + // Prepare error description for timeout event + bmqu::MemOutStream errorStream; + errorStream << "Reauthentication not received within authenticated " + "lifetime of " + << lifetime << " ms"; + + scheduler_p->scheduleEvent( + &d_timeoutHandle, + bsls::TimeInterval(bmqsys::Time::nowMonotonicClock()) + .addMilliseconds(lifetime), + bdlf::BindUtil::bind( + bmqu::WeakMemFnUtil::weakMemFn( + &AuthenticationContext::onReauthenticateErrorOrTimeout, + d_self.acquireWeak()), + -1, // errorCode + "authenticationTimeout", // errorName + errorStream.str(), // errorDescription + channel // channel + )); + } + + return 0; +} + +void AuthenticationContext::onReauthenticateErrorOrTimeout( + int errorCode, + const bsl::string& errorName, + const bsl::string& errorDescription, + const bsl::shared_ptr& channel) +{ + // PRECONDITIONS + BSLS_ASSERT_SAFE(channel); + + { + bslmt::LockGuard guard(&d_mutex); // LOCKED + + if (d_state == AuthenticationState::e_CLOSED) { + return; + } + } // UNLOCK + + BALL_LOG_ERROR << "Reauthentication error or timeout for '" + << channel->peerUri() << "' [error: " << errorDescription + << ", code: " << errorCode << "]"; + + bmqio::Status status(bmqio::StatusCategory::e_CANCELED, + errorName, + errorCode, + d_allocator_p); + channel->close(status); +} + +void AuthenticationContext::onClose(bdlmt::EventScheduler* scheduler_p) +{ + // executed by *ANY* thread + + bslmt::LockGuard guard(&d_mutex); // LOCKED + + if (d_state == AuthenticationState::e_CLOSED) { + return; // idempotent + } + d_state = AuthenticationState::e_CLOSED; + + if (d_timeoutHandle) { + scheduler_p->cancelEventAndWait(&d_timeoutHandle); + } +} + +bool AuthenticationContext::tryStartReauthentication() +{ + bslmt::LockGuard guard(&d_mutex); // LOCKED + + if (d_state == AuthenticationState::e_AUTHENTICATED) { + d_state = AuthenticationState::e_AUTHENTICATING; + return true; + } + + return false; +} + +const bsl::shared_ptr& +AuthenticationContext::authenticationResult() const +{ + return d_authenticationResultSp; +} + +const bmqp_ctrlmsg::AuthenticationMessage& +AuthenticationContext::authenticationMessage() const +{ + // PRECONDITION + BSLS_ASSERT_SAFE(d_state == AuthenticationState::e_AUTHENTICATING); + + return d_authenticationMessage; +} + +bmqp::EncodingType::Enum AuthenticationContext::encodingType() const +{ + // PRECONDITION + BSLS_ASSERT_SAFE(d_state == AuthenticationState::e_AUTHENTICATING); + + return d_encodingType; +} + +InitialConnectionContext* AuthenticationContext::initialConnectionContext() +{ + return d_initialConnectionContext_p; +} + +} // namespace mqbnet +} // namespace BloombergLP diff --git a/src/groups/mqb/mqbnet/mqbnet_authenticationcontext.h b/src/groups/mqb/mqbnet/mqbnet_authenticationcontext.h new file mode 100644 index 0000000000..d635c7a239 --- /dev/null +++ b/src/groups/mqb/mqbnet/mqbnet_authenticationcontext.h @@ -0,0 +1,253 @@ +// Copyright 2025 Bloomberg Finance L.P. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// mqbnet_authenticationcontext.h -*-C++-*- +#ifndef INCLUDED_MQBNET_AUTHENTICATIONCONTEXT +#define INCLUDED_MQBNET_AUTHENTICATIONCONTEXT + +/// @file mqbnet_authenticationcontext.h +/// +/// @brief Provide the context for authenticating connections. +/// +/// An instance is created per connection being authenticated. It tracks +/// the authentication state, the resulting principal, and (for the +/// initial pass) the associated InitialConnectionContext. + +// MQB +#include + +// BMQ +#include +#include +#include + +// BDE +#include +#include +#include +#include +#include +#include + +namespace BloombergLP { + +// FORWARD DECLARATION +namespace bmqio { +class Channel; +} +namespace mqbplug { +class AuthenticationResult; +} +namespace mqbnet { +class InitialConnectionContext; +} + +namespace mqbnet { + +// =========================== +// class AuthenticationContext +// =========================== + +struct AuthenticationState { + enum Enum { + e_AUTHENTICATING = 0, // Authentication is in progress. + e_AUTHENTICATED, // Authentication is completed. + e_CLOSED // Channel is closed. + }; + + // CLASS METHODS + + /// Write the string representation of the specified enumeration + /// `value` to the specified output `stream`, and return a reference to + /// `stream`. Optionally specify an initial indentation `level`, whose + /// absolute value is incremented recursively for nested objects. If + /// `level` is specified, optionally specify `spacesPerLevel`, whose + /// absolute value indicates the number of spaces per indentation level + /// for this and all of its nested objects. If `level` is negative, + /// suppress indentation of the first line. If `spacesPerLevel` is + /// negative, format the entire output on one line, suppressing all but + /// the initial indentation (as governed by `level`). See `toAscii` + /// for what constitutes the string representation of a + /// @bbref{AuthenticationState::Enum} value. + static bsl::ostream& print(bsl::ostream& stream, + AuthenticationState::Enum value, + int level = 0, + int spacesPerLevel = 4); + + /// Return the non-modifiable string representation corresponding to + /// the specified enumeration `value`, if it exists, and a unique + /// (error) string otherwise. The string representation of `value` + /// matches its corresponding enumerator name with the `e_` prefix + /// elided. Note that specifying a `value` that does not match any of + /// the enumerators will result in a string representation that is + /// distinct from any of those corresponding to the enumerators, but is + /// otherwise unspecified. + static const char* toAscii(AuthenticationState::Enum value); + + /// Return true and fills the specified `out` with the enum value + /// corresponding to the specified `str`, if valid, or return false and + /// leave `out` untouched if `str` doesn't correspond to any value of + /// the enum. + static bool fromAscii(AuthenticationState::Enum* out, + const bsl::string_view str); +}; + +// FREE OPERATORS + +/// Format the specified `value` to the specified output `stream` and return +/// a reference to the modifiable `stream`. +bsl::ostream& operator<<(bsl::ostream& stream, + AuthenticationState::Enum value); + +/// VST for the context associated with an connection being authenticated. +class AuthenticationContext { + private: + // CLASS-SCOPE CATEGORY + BALL_LOG_SET_CLASS_CATEGORY("MQBNET.AUTHENTICATIONCONTEXT"); + + public: + // TYPES + typedef bdlmt::EventScheduler::EventHandle EventHandle; + + private: + // DATA + + /// Allocator to use. + bslma::Allocator* d_allocator_p; + + /// Used to make sure no callback is invoked on a destroyed object. + bmqu::SharedResource d_self; + + /// Mutex to protect the state and timeoutHandle of this object. + bslmt::Mutex d_mutex; + + /// The authentication result to be used for authorization. It is first + /// set during the initial authentication, and can be updated later during + /// reauthentication. + bsl::shared_ptr d_authenticationResultSp; + + /// Handle to the reauthentication timer event, if any. + EventHandle d_timeoutHandle; + + /// Authentication State. + AuthenticationState::Enum d_state; + + /// The initial connection context associated with this authentication + /// context. It is set during the initial authentication, and is null for + /// reauthentication. + InitialConnectionContext* d_initialConnectionContext_p; + + /// The authentication message received during authentication. This is + /// held temporarily and cleared after authentication completes. + bmqp_ctrlmsg::AuthenticationMessage d_authenticationMessage; + + /// The encoding type used for sending authentication responses. The value + /// matches the encoding type set by the client during authentication and + /// reauthentication and ensures that the response is sent using the same + /// encoding. + bmqp::EncodingType::Enum d_encodingType; + + private: + // NOT IMPLEMENTED + + /// Copy constructor and assignment operator are not implemented. + AuthenticationContext(const AuthenticationContext&); // = delete; + AuthenticationContext& + operator=(const AuthenticationContext&); // = delete; + + public: + // TRAITS + BSLMF_NESTED_TRAIT_DECLARATION(AuthenticationContext, + bslma::UsesBslmaAllocator) + // CREATORS + AuthenticationContext( + InitialConnectionContext* initialConnectionContext, + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMessage, + bmqp::EncodingType::Enum authenticationEncodingType, + AuthenticationState::Enum state, + bslma::Allocator* allocator = 0); + + // MANIPULATORS + void setAuthenticationResult( + const bsl::shared_ptr& value); + + // NOTE: AuthenticationMessage and encodingType are set only during + // reauthentication when AuthenticationState is e_AUTHENTICATED. + // authenticationMessage() and encodingType() are called only when + // AuthenticationState is e_AUTHENTICATING. Hence, no need for mutex + // protection. + void + setAuthenticationMessage(const bmqp_ctrlmsg::AuthenticationMessage& value); + void setAuthenticationEncodingType(bmqp::EncodingType::Enum value); + + void resetAuthenticationMessage(); + + /// Schedule a reauthentication timer using the specified `scheduler_p` + /// with the specified `lifetimeMs`. The specified `channel` is used to + /// close the connection in case of reauthentication timeout or error. + /// Return 0 on success, and a non-zero value populating the specified + /// `errorDescription` with details on failure. + int setAuthenticatedAndScheduleReauthn( + bsl::ostream& errorDescription, + bdlmt::EventScheduler* scheduler_p, + const bsl::optional& lifetimeMs, + const bsl::shared_ptr& channel); + + /// Close the specified `channel` with `errorCode`, `errorName`, and + /// `errorDescription`, indicating a reauthentication error or + /// authentication timeout for the current context. + void onReauthenticateErrorOrTimeout( + const int errorCode, + const bsl::string& errorName, + const bsl::string& errorDescription, + const bsl::shared_ptr& channel); + + /// Called when a channel is closing. Cancel any outstanding + /// reauthentication timer using the specified `scheduler_p`. + void onClose(bdlmt::EventScheduler* scheduler_p); + + /// Attempt to begin reauthentication by transitioning the state from + /// AUTHENTICATED to AUTHENTICATING. + /// Return true if the transition occurred (i.e. state was AUTHENTICATED); + /// otherwise return false (already authenticating, closed, or not yet + /// authenticated). + bool tryStartReauthentication(); + + // ACCESSORS + const bsl::shared_ptr& + authenticationResult() const; + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMessage() const; + bmqp::EncodingType::Enum encodingType() const; + + InitialConnectionContext* initialConnectionContext(); +}; + +} // close package namespace + +// -------------------------- +// struct AuthenticationState +// -------------------------- + +// FREE OPERATORS +inline bsl::ostream& +mqbnet::operator<<(bsl::ostream& stream, + mqbnet::AuthenticationState::Enum value) +{ + return AuthenticationState::print(stream, value, 0, -1); +} + +} // close enterprise namespace + +#endif diff --git a/src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.cpp b/src/groups/mqb/mqbnet/mqbnet_authenticator.cpp similarity index 74% rename from src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.cpp rename to src/groups/mqb/mqbnet/mqbnet_authenticator.cpp index 82e0164292..aac70fcda0 100644 --- a/src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.cpp +++ b/src/groups/mqb/mqbnet/mqbnet_authenticator.cpp @@ -13,19 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -// mqbnet_initialconnectionhandler.cpp -*-C++-*- -#include +// mqbnet_authenticator.cpp -*-C++-*- +#include #include namespace BloombergLP { namespace mqbnet { -// ------------------------------ -// class InitialConnectionHandler -// ------------------------------ +// ------------------- +// class Authenticator +// ------------------- -InitialConnectionHandler::~InitialConnectionHandler() +Authenticator::~Authenticator() { // NOTHING: Pure interface } diff --git a/src/groups/mqb/mqbnet/mqbnet_authenticator.h b/src/groups/mqb/mqbnet/mqbnet_authenticator.h new file mode 100644 index 0000000000..1a8dd1311d --- /dev/null +++ b/src/groups/mqb/mqbnet/mqbnet_authenticator.h @@ -0,0 +1,102 @@ +// Copyright 2025 Bloomberg Finance L.P. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// mqbnet_authenticator.h -*-C++-*- +#ifndef INCLUDED_MQBNET_AUTHENTICATOR +#define INCLUDED_MQBNET_AUTHENTICATOR + +/// @file mqbnet_authenticator.h +/// +/// @brief Provide a protocol for an authenticator. +/// +/// @bbref{mqbnet::Authenticator} is a protocol for an authenticator that +/// (re)authenticates a connection with a BlazingMQ client or another bmqbrkr. + +// MQB +#include +#include + +// BMQ +#include + +// BDE +#include +#include +#include + +namespace BloombergLP { + +namespace mqbnet { + +// FORWARD DECLARATION +class InitialConnectionContext; + +// =================== +// class Authenticator +// =================== + +/// Protocol for an Authenticator +class Authenticator { + public: + // CREATORS + + /// Destructor + virtual ~Authenticator(); + + // MANIPULATORS + + /// Start the authenticator. Return 0 on success, or a non-zero error + /// code and populate the specified `errorDescription` with a description + /// of the error otherwise. + virtual int start(bsl::ostream& errorDescription) = 0; + + /// Stop the authenticator. + virtual void stop() = 0; + + /// Authenticate the connection based on the type of AuthenticationMessage + /// in the specified `context`. + /// Return 0 on success, or a non-zero error code and populate the + /// specified `errorDescription` with a description of the error otherwise. + virtual int handleAuthentication( + bsl::ostream& errorDescription, + const bsl::shared_ptr& context, + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMsg) = 0; + + virtual int handleReauthentication( + bsl::ostream& errorDescription, + const bsl::shared_ptr& context, + const bsl::shared_ptr& channel) = 0; + + /// Produce and send outbound authentication message with the specified + /// `context`. Return 0 on success, or a non-zero error code and populate + /// the specified `errorDescription` with a description of the error + /// otherwise. + /// TODO: Rethink the need for this method in the interface. + virtual int authenticationOutbound( + bsl::ostream& errorDescription, + const bsl::shared_ptr& context) = 0; + + // ACCESSORS + + /// Return the anonymous credential used for authentication. + /// If no anonymous credential is set, return an empty optional. + virtual const bsl::optional& + anonymousCredential() const = 0; +}; + +} // close package namespace +} // close enterprise namespace + +#endif diff --git a/src/groups/mqb/mqbnet/mqbnet_channel.cpp b/src/groups/mqb/mqbnet/mqbnet_channel.cpp index 1d0ccbce47..9aa98c9828 100644 --- a/src/groups/mqb/mqbnet/mqbnet_channel.cpp +++ b/src/groups/mqb/mqbnet/mqbnet_channel.cpp @@ -458,6 +458,7 @@ Channel::writeBufferedItem(bool* isConsumed, case bmqp::EventType::e_HEARTBEAT_REQ: case bmqp::EventType::e_HEARTBEAT_RSP: case bmqp::EventType::e_REPLICATION_RECEIPT: + case bmqp::EventType::e_AUTHENTICATION: default: { ControlArgs x(item); rc = writeImmediate(isConsumed, diff --git a/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontex.t.cpp b/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontex.t.cpp new file mode 100644 index 0000000000..27f32292cf --- /dev/null +++ b/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontex.t.cpp @@ -0,0 +1,211 @@ +// Copyright 2023 Bloomberg Finance L.P. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// mqbnet_initialconnectioncontext.t.cpp -*-C++-*- +#include + +// MQB +#include +#include +#include + +// BMQ +#include + +// BDE +#include +#include +#include + +// TEST DRIVER +#include + +// CONVENIENCE +using namespace BloombergLP; +using namespace bsl; + +namespace { + +void complete(const bsl::shared_ptr& check, + int status, + const bsl::string& errorDescription, + const bsl::shared_ptr& session, + const bsl::shared_ptr& channel, + const mqbnet::InitialConnectionContext* initialConnectionContext) +{ + BSLS_ASSERT_SAFE(check); + + BSLS_ASSERT_SAFE(*check == 0); + + *check = status; + + (void)errorDescription; + (void)session; + (void)channel; + (void)initialConnectionContext; +} + +// Mock authenticator +struct MockAuthenticator : public mqbnet::Authenticator { + private: + bsl::optional d_anonymousCredential; + + public: + int start(bsl::ostream&) BSLS_KEYWORD_OVERRIDE { return 0; } + void stop() BSLS_KEYWORD_OVERRIDE {} + int handleAuthentication( + bsl::ostream&, + const bsl::shared_ptr&, + const bmqp_ctrlmsg::AuthenticationMessage&) BSLS_KEYWORD_OVERRIDE + { + return 0; + } + int handleReauthentication( + bsl::ostream&, + const bsl::shared_ptr&, + const bsl::shared_ptr&) BSLS_KEYWORD_OVERRIDE + { + return 0; + } + int authenticationOutbound( + bsl::ostream&, + const bsl::shared_ptr&) + BSLS_KEYWORD_OVERRIDE + { + return 0; + } + const bsl::optional& + anonymousCredential() const BSLS_KEYWORD_OVERRIDE + { + return d_anonymousCredential; + } +}; + +// Mock negotiator +struct MockNegotiator : public mqbnet::Negotiator { + int createSessionOnMsgType(bsl::ostream&, + bsl::shared_ptr*, + mqbnet::InitialConnectionContext*) + BSLS_KEYWORD_OVERRIDE + { + return 0; + } + int + negotiateOutbound(bsl::ostream&, + const bsl::shared_ptr&) + BSLS_KEYWORD_OVERRIDE + { + return 0; + } +}; + +} // close unnamed namespace + +// ============================================================================ +// TESTS +// ---------------------------------------------------------------------------- + +static void test1_initialConnectionContext() +{ + bslma::Allocator* alloc = bmqtst::TestHelperUtil::allocator(); + bsl::shared_ptr authenticator = + bsl::allocate_shared(alloc); + bsl::shared_ptr negotiator = + bsl::allocate_shared(alloc); + bsl::shared_ptr channel = + bsl::allocate_shared(alloc); + + bsl::shared_ptr check = bsl::allocate_shared(alloc, 0); + mqbnet::InitialConnectionContext::InitialConnectionCompleteCb completeCb = + bdlf::BindUtil::bind( + &complete, + check, + bdlf::PlaceHolders::_1, // status + bdlf::PlaceHolders::_2, // errorDescription + bdlf::PlaceHolders::_3, // session + bdlf::PlaceHolders::_4, // channel + bdlf::PlaceHolders::_5 // initialConnectionContext + ); + + bmqtst::TestHelper::printTestName("test1_basicConstruction"); + { + PV("Constructor"); + mqbnet::InitialConnectionContext obj1(false, + authenticator.get(), + negotiator.get(), + static_cast(0), + static_cast(0), + channel, + completeCb); + BMQTST_ASSERT_EQ(obj1.isIncoming(), false); + BMQTST_ASSERT_EQ(obj1.resultState(), static_cast(0)); + BMQTST_ASSERT_EQ(obj1.userData(), static_cast(0)); + } + + { + PV("Manipulators/Accessors"); + + mqbnet::InitialConnectionContext obj(true, + authenticator.get(), + negotiator.get(), + static_cast(0), + static_cast(0), + channel, + completeCb); + + { // ResultState + int value = 9; + obj.setResultState(&value); + BMQTST_ASSERT_EQ(obj.resultState(), &value); + } + + { // AuthenticationContext + bsl::shared_ptr authnCtx = + bsl::allocate_shared(alloc); + obj.setAuthenticationContext(authnCtx); + BMQTST_ASSERT_EQ(authnCtx, obj.authenticationContext()); + } + + { + // CompletionCb + int rc = 1; + obj.complete(rc, + bsl::string(), + bsl::shared_ptr()); + + BMQTST_ASSERT_EQ(*check, rc); + } + } +} + +// ============================================================================ +// MAIN PROGRAM +// ---------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + TEST_PROLOG(bmqtst::TestHelper::e_DEFAULT); + + switch (_testCase) { + case 0: + case 1: test1_initialConnectionContext(); break; + default: { + cerr << "WARNING: CASE '" << _testCase << "' NOT FOUND." << endl; + bmqtst::TestHelperUtil::testStatus() = -1; + } break; + } + + TEST_EPILOG(bmqtst::TestHelper::e_CHECK_GBL_ALLOC); +} diff --git a/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.cpp b/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.cpp index b253ceee2a..f5b9cecb57 100644 --- a/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.cpp +++ b/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.cpp @@ -18,21 +18,210 @@ #include +// MQB +#include +#include +#include + +// BMQ +#include +#include +#include +#include +#include +#include + +// BDE +#include +#include +#include +#include +#include + namespace BloombergLP { namespace mqbnet { -// ------------------------------------- +namespace { + +const int k_INITIALCONNECTION_READTIMEOUT = 3 * 60; // 3 minutes + +// Trampoline that captures a shared_ptr to extend lifetime. +void readCallbackTrampoline( + const bsl::shared_ptr& self, + const BloombergLP::bmqio::Status& status, + int* numNeeded, + BloombergLP::bdlbb::Blob* blob) +{ + self->readCallback(status, numNeeded, blob); +} + +} + +// ----------------------------- +// struct InitialConnectionState +// ----------------------------- + +bsl::ostream& InitialConnectionState::print(bsl::ostream& stream, + InitialConnectionState::Enum value, + int level, + int spacesPerLevel) +{ + if (stream.bad()) { + return stream; // RETURN + } + + bdlb::Print::indent(stream, level, spacesPerLevel); + stream << InitialConnectionState::toAscii(value); + + if (spacesPerLevel >= 0) { + stream << '\n'; + } + + return stream; +} + +const char* InitialConnectionState::toAscii(InitialConnectionState::Enum value) +{ +#define CASE(X) \ + case e_##X: return #X; + + switch (value) { + CASE(INITIAL) + CASE(AUTHENTICATING) + CASE(AUTHENTICATED) + CASE(ANON_AUTHENTICATING) + CASE(NEGOTIATING_OUTBOUND) + CASE(NEGOTIATED) + CASE(FAILED) + default: return "(* UNKNOWN *)"; + } + +#undef CASE +} + +bool InitialConnectionState::fromAscii(InitialConnectionState::Enum* out, + const bslstl::StringRef& str) +{ +#define CHECKVALUE(M) \ + if (bdlb::String::areEqualCaseless( \ + toAscii(InitialConnectionState::e_##M), \ + str.data(), \ + static_cast(str.length()))) { \ + *out = InitialConnectionState::e_##M; \ + return true; \ + } + + CHECKVALUE(INITIAL) + CHECKVALUE(AUTHENTICATING) + CHECKVALUE(AUTHENTICATED) + CHECKVALUE(ANON_AUTHENTICATING) + CHECKVALUE(NEGOTIATING_OUTBOUND) + CHECKVALUE(NEGOTIATED) + CHECKVALUE(FAILED) + + // Invalid string + return false; + +#undef CHECKVALUE +} + +// ----------------------------- +// struct InitialConnectionEvent +// ----------------------------- + +bsl::ostream& InitialConnectionEvent::print(bsl::ostream& stream, + InitialConnectionEvent::Enum value, + int level, + int spacesPerLevel) +{ + if (stream.bad()) { + return stream; // RETURN + } + + bdlb::Print::indent(stream, level, spacesPerLevel); + stream << InitialConnectionEvent::toAscii(value); + + if (spacesPerLevel >= 0) { + stream << '\n'; + } + + return stream; +} + +const char* InitialConnectionEvent::toAscii(InitialConnectionEvent::Enum value) +{ +#define CASE(X) \ + case e_##X: return #X; + + switch (value) { + CASE(NONE) + CASE(OUTBOUND_NEGOTATION) + CASE(INCOMING) + CASE(AUTHN_REQUEST) + CASE(NEGOTIATION_MESSAGE) + CASE(AUTHN_SUCCESS) + CASE(ERROR) + default: return "(* UNKNOWN *)"; + } + +#undef CASE +} + +bool InitialConnectionEvent::fromAscii(InitialConnectionEvent::Enum* out, + const bslstl::StringRef& str) +{ +#define CHECKVALUE(M) \ + if (bdlb::String::areEqualCaseless( \ + toAscii(InitialConnectionEvent::e_##M), \ + str.data(), \ + static_cast(str.length()))) { \ + *out = InitialConnectionEvent::e_##M; \ + return true; \ + } + + CHECKVALUE(NONE) + CHECKVALUE(OUTBOUND_NEGOTATION) + CHECKVALUE(INCOMING) + CHECKVALUE(AUTHN_REQUEST) + CHECKVALUE(NEGOTIATION_MESSAGE) + CHECKVALUE(AUTHN_SUCCESS) + CHECKVALUE(ERROR) + + // Invalid string + return false; + +#undef CHECKVALUE +} + +// ------------------------------ // class InitialConnectionContext -// ------------------------------------- +// ------------------------------ -InitialConnectionContext::InitialConnectionContext(bool isIncoming) -: d_resultState_p(0) -, d_userData_p(0) -, d_channelSp() -, d_initialConnectionCompleteCb() +// CREATORS +InitialConnectionContext::InitialConnectionContext( + bool isIncoming, + mqbnet::Authenticator* authenticator, + mqbnet::Negotiator* negotiator, + void* userData, + void* resultState, + const bsl::shared_ptr& channel, + const InitialConnectionCompleteCb& initialConnectionCompleteCb, + bslma::Allocator* allocator) +: d_allocator_p(allocator) +, d_mutex() +, d_authenticator_p(authenticator) +, d_negotiator_p(negotiator) +, d_resultState_p(resultState) +, d_userData_p(userData) +, d_channelSp(channel) +, d_authenticationCtxSp() , d_negotiationCtxSp() +, d_initialConnectionCompleteCb(initialConnectionCompleteCb) +, d_authenticationEncodingType(bmqp::EncodingType::e_BER) +, d_state(InitialConnectionState::e_INITIAL) , d_isIncoming(isIncoming) , d_isClosed(false) + { // NOTHING } @@ -42,37 +231,237 @@ InitialConnectionContext::~InitialConnectionContext() // NOTHING } -InitialConnectionContext& InitialConnectionContext::setUserData(void* value) +// PRIVATE MANIPULATORS +void InitialConnectionContext::setState(InitialConnectionState::Enum value) { - d_userData_p = value; - return *this; + d_state = value; } -InitialConnectionContext& InitialConnectionContext::setResultState(void* value) +int InitialConnectionContext::scheduleRead(bsl::ostream& errorDescription) { - d_resultState_p = value; - return *this; + bsl::shared_ptr self = shared_from_this(); + + // Schedule a TimedRead + bmqio::Status status; + channel()->read(&status, + bmqp::Protocol::k_PACKET_MIN_SIZE, + bdlf::BindUtil::bind(&readCallbackTrampoline, + self, + bdlf::PlaceHolders::_1, // status + bdlf::PlaceHolders::_2, // numNeeded + bdlf::PlaceHolders::_3), // blob + bsls::TimeInterval(k_INITIALCONNECTION_READTIMEOUT)); + + if (!status) { + errorDescription << "Read failed while negotiating: " << status; + return -1; // RETURN + } + + return 0; +} + +int InitialConnectionContext::readBlob(bsl::ostream& errorDescription, + bdlbb::Blob* outPacket, + bool* isFullBlob, + int* numNeeded, + bdlbb::Blob* blob) +{ + int rc = bmqio::ChannelUtil::handleRead(outPacket, numNeeded, blob); + if (rc != 0) { + // This indicates a non recoverable error... + errorDescription << "Unrecoverable read error:\n" + << bmqu::BlobStartHexDumper(blob); + return -1; // RETURN + } + + if (outPacket->length() == 0) { + // Don't yet have a full blob + *isFullBlob = false; + return 0; // RETURN + } + + // Have a full blob, indicate no more bytes needed (we have to do this + // because 'handleRead' above set it back to 4 at the end). + *numNeeded = 0; + + return 0; +} + +int InitialConnectionContext::processBlob(bsl::ostream& errorDescription, + const bdlbb::Blob& blob) +{ + // executed by one of the *IO* threads + + bsl::variant + message; + + int rc = decodeInitialConnectionMessage(errorDescription, &message, blob); + + if (rc != 0) { + errorDescription << "Failed to decode initial connection message"; + return -1; // RETURN + } + + if (bsl::holds_alternative(message)) { + errorDescription << "Decode AuthenticationMessage or " + "NegotiationMessage succeeds but nothing gets " + "loaded in."; + return -1; + } + else if (bsl::holds_alternative( + message)) { + handleEvent(bsl::string(), + InitialConnectionEvent::e_AUTHN_REQUEST, + message); + } + else { + handleEvent(bsl::string(), + InitialConnectionEvent::e_NEGOTIATION_MESSAGE, + message); + } + + return 0; +} + +int InitialConnectionContext::decodeInitialConnectionMessage( + bsl::ostream& errorDescription, + bsl::variant* message, + const bdlbb::Blob& blob) +{ + BSLS_ASSERT(message); + + enum RcEnum { + // Value for the various RC error categories + rc_SUCCESS = 0, + rc_INVALID_MESSAGE = -1, + rc_INVALID_EVENT = -2, + rc_INVALID_AUTHENTICATION_EVENT = -3, + rc_INVALID_CONTROL_EVENT = -4 + }; + + bdlma::LocalSequentialAllocator<2048> localAllocator(d_allocator_p); + + bmqp::Event event(&blob, &localAllocator); + + if (!event.isValid()) { + errorDescription << "Invalid negotiation message received " + << "(packet is not a valid BlazingMQ event):\n" + << bmqu::BlobStartHexDumper(&blob); + return rc_INVALID_MESSAGE; // RETURN + } + + bmqp_ctrlmsg::AuthenticationMessage authenticationMessage; + bmqp_ctrlmsg::NegotiationMessage negotiationMessage; + + if (event.isAuthenticationEvent()) { + BALL_LOG_DEBUG << "Received AuthenticationEvent: " + << bmqu::BlobStartHexDumper(&blob); + const int rc = event.loadAuthenticationEvent(&authenticationMessage); + if (rc != 0) { + errorDescription + << "Invalid message received [reason: 'authentication " + "event is not an AuthenticationMessage', rc: " + << rc << "]:" << bmqu::BlobStartHexDumper(&blob); + return rc_INVALID_AUTHENTICATION_EVENT; // RETURN + } + + d_authenticationEncodingType = event.authenticationEventEncodingType(); + *message = authenticationMessage; + } + else if (event.isControlEvent()) { + BALL_LOG_DEBUG << "Received ControlEvent: " + << bmqu::BlobStartHexDumper(&blob); + const int rc = event.loadControlEvent(&negotiationMessage); + if (rc != 0) { + errorDescription << "Invalid message received [reason: 'control " + "event is not a NegotiationMessage', rc: " + << rc << "]:" << bmqu::BlobStartHexDumper(&blob); + return rc_INVALID_CONTROL_EVENT; // RETURN + } + + *message = negotiationMessage; + } + else { + errorDescription + << "Invalid initial connection message received " + << "(packet is not an AuthenticationEvent or ControlEvent):\n" + << bmqu::BlobStartHexDumper(&blob); + return rc_INVALID_EVENT; // RETURN + } + + return rc_SUCCESS; +} + +void InitialConnectionContext::createNegotiationContext() +{ + if (d_negotiationCtxSp) { + return; // RETURN + } + + d_negotiationCtxSp = bsl::allocate_shared( + d_allocator_p, + this // initialConnectionContext + ); +} + +int InitialConnectionContext::handleAnonAuthentication( + bsl::ostream& errorDescription) +{ + // executed by one of the *IO* threads + + // PRECONDITIONS + BSLS_ASSERT_SAFE(!authenticationContext()); + + if (!d_authenticator_p->anonymousCredential()) { + errorDescription << "Anonymous credential is disallowed, " + << "cannot negotiate without authentication."; + return -1; // RETURN + } + + bmqp_ctrlmsg::AuthenticationMessage authenticationMessage; + bmqp_ctrlmsg::AuthenticationRequest& authenticationRequest = + authenticationMessage.makeAuthenticationRequest(); + + const mqbcfg::Credential& anonymousCredential = + d_authenticator_p->anonymousCredential().value(); + authenticationRequest.mechanism() = anonymousCredential.mechanism(); + authenticationRequest.data() = bsl::vector( + anonymousCredential.identity().begin(), + anonymousCredential.identity().end()); + + bsl::shared_ptr self = shared_from_this(); + + const int rc = d_authenticator_p->handleAuthentication( + errorDescription, + self, + authenticationMessage); + + return rc; } -InitialConnectionContext& InitialConnectionContext::setChannel( - const bsl::shared_ptr& value) +// MANIPULATORS +void InitialConnectionContext::setResultState(void* value) { - d_channelSp = value; - return *this; + d_resultState_p = value; } -InitialConnectionContext& InitialConnectionContext::setCompleteCb( - const InitialConnectionCompleteCb& value) +void InitialConnectionContext::setAuthenticationContext( + const bsl::shared_ptr& value) { - d_initialConnectionCompleteCb = value; - return *this; + // PRECONDITIONS + BSLS_ASSERT_SAFE(!d_authenticationCtxSp); + + d_authenticationCtxSp = value; } -InitialConnectionContext& InitialConnectionContext::setNegotiationContext( +void InitialConnectionContext::setNegotiationContext( const bsl::shared_ptr& value) { d_negotiationCtxSp = value; - return *this; } void InitialConnectionContext::onClose() @@ -80,6 +469,251 @@ void InitialConnectionContext::onClose() d_isClosed = true; } +void InitialConnectionContext::readCallback(const bmqio::Status& status, + int* numNeeded, + bdlbb::Blob* blob) +{ + // executed by one of the *IO* threads + + BALL_LOG_TRACE << "InitialConnectionContext readCb: [status: " << status + << ", peer: '" << channel()->peerUri() << "']"; + + bdlbb::Blob outPacket; + bool isFullBlob = true; + bmqu::MemOutStream errStream; + int rc = 0; + + if (!status) { + errStream << "Read error: " << status; + handleEvent(errStream.str(), InitialConnectionEvent::e_ERROR); + return; // RETURN + } + + rc = readBlob(errStream, &outPacket, &isFullBlob, numNeeded, blob); + if (rc != 0) { + handleEvent(errStream.str(), InitialConnectionEvent::e_ERROR); + return; // RETURN + } + + if (!isFullBlob) { + return; // RETURN + } + + rc = processBlob(errStream, outPacket); + if (rc != 0) { + handleEvent(errStream.str(), InitialConnectionEvent::e_ERROR); + return; // RETURN + } +} + +void InitialConnectionContext::handleInitialConnection() +{ + if (!isIncoming()) { + // TODO: When we are ready to move on to the next step, we should + // call `authenticationOutbound` here instead before calling + // `negotiateOutbound`. + handleEvent(bsl::string(), + InitialConnectionEvent::e_OUTBOUND_NEGOTATION); + } + else { + bmqu::MemOutStream errStream; + const int rc = scheduleRead(errStream); + if (rc != 0) { + handleEvent(errStream.str(), InitialConnectionEvent::e_ERROR); + } + } +} + +void InitialConnectionContext::handleEvent( + const bsl::string& errorDescription, + InitialConnectionEvent::Enum event, + const bsl::variant& message) +{ + // executed by an *AUTHENTICATION* or one of the *IO* threads + + enum RcEnum { + // Value for the various RC error categories + rc_SUCCESS = 0, + rc_ERROR = -1 + }; + + bsl::shared_ptr self = shared_from_this(); + + bmqu::MemOutStream errStream(d_allocator_p); + int rc = rc_ERROR; + + bslmt::LockGuard guard(&d_mutex); // LOCKED + + BALL_LOG_INFO << "Enter InitialConnectionContext::handleEvent: " + << "state = " << d_state << ", event = " << event + << "; peerUri = " << d_channelSp->peerUri() + << "; context address = " << this; + + InitialConnectionState::Enum oldState = d_state; + + switch (event) { + case InitialConnectionEvent::e_OUTBOUND_NEGOTATION: { + if (oldState == InitialConnectionState::e_INITIAL) { + setState(InitialConnectionState::e_NEGOTIATING_OUTBOUND); + + createNegotiationContext(); + + rc = d_negotiator_p->negotiateOutbound(errStream, self); + if (rc == rc_SUCCESS) { + rc = scheduleRead(errStream); + } + } + else { + errStream << "Unexpected event received: " << oldState << " -> " + << event; + BALL_LOG_ERROR << "#UNEXPECTED_STATE " << errStream.str() + << " [peer: " << channel()->peerUri() << "]"; + } + break; + } + case InitialConnectionEvent::e_INCOMING: { + if (oldState == InitialConnectionState::e_INITIAL) { + // For incoming connections, start reading the first message + rc = scheduleRead(errStream); + } + else { + errStream << "Unexpected event received: " << oldState << " -> " + << event; + BALL_LOG_ERROR << "#UNEXPECTED_STATE " << errStream.str() + << " [peer: " << channel()->peerUri() << "]"; + } + break; + } + case InitialConnectionEvent::e_AUTHN_REQUEST: { + BSLS_ASSERT_SAFE( + bsl::holds_alternative( + message)); + const bmqp_ctrlmsg::AuthenticationMessage& authenticationMsg = + bsl::get(message); + + if (oldState == InitialConnectionState::e_INITIAL) { + setState(InitialConnectionState::e_AUTHENTICATING); + + rc = d_authenticator_p->handleAuthentication(errStream, + self, + authenticationMsg); + } + else { + errStream << "Unexpected event received: " << oldState << " -> " + << event; + BALL_LOG_ERROR << "#UNEXPECTED_STATE " << errStream.str() + << " [peer: " << channel()->peerUri() << "]"; + } + break; + } + case InitialConnectionEvent::e_NEGOTIATION_MESSAGE: { + BSLS_ASSERT_SAFE( + bsl::holds_alternative(message)); + const bmqp_ctrlmsg::NegotiationMessage& negotiationMsg = + bsl::get(message); + + if (oldState == InitialConnectionState::e_INITIAL && + negotiationMsg.isClientIdentityValue()) { + setState(InitialConnectionState::e_ANON_AUTHENTICATING); + + createNegotiationContext(); + negotiationContext()->setNegotiationMessage(negotiationMsg); + + rc = handleAnonAuthentication(errStream); + } + else if (oldState == InitialConnectionState::e_AUTHENTICATED && + negotiationMsg.isClientIdentityValue()) { + setState(InitialConnectionState::e_NEGOTIATED); + + createNegotiationContext(); + negotiationContext()->setNegotiationMessage(negotiationMsg); + + rc = rc_SUCCESS; + } + else if (oldState == InitialConnectionState::e_NEGOTIATING_OUTBOUND && + negotiationMsg.isBrokerResponseValue()) { + setState(InitialConnectionState::e_NEGOTIATED); + + BSLS_ASSERT_SAFE(negotiationContext()); + negotiationContext()->setNegotiationMessage(negotiationMsg); + + rc = rc_SUCCESS; + } + else { + errStream << "Unexpected event received: " << oldState << " -> " + << event << " [ negotiationMsg: " << negotiationMsg + << " ]"; + BALL_LOG_ERROR << "#UNEXPECTED_STATE " << errStream.str() + << " [peer: " << channel()->peerUri() << "]"; + } + break; + } + case InitialConnectionEvent::e_AUTHN_SUCCESS: { + if (oldState == InitialConnectionState::e_AUTHENTICATING) { + setState(InitialConnectionState::e_AUTHENTICATED); + + // Now read Negotiation message + rc = scheduleRead(errStream); + } + else if (oldState == InitialConnectionState::e_ANON_AUTHENTICATING) { + setState(InitialConnectionState::e_NEGOTIATED); + + BSLS_ASSERT_SAFE(negotiationContext()); + BSLS_ASSERT_SAFE(negotiationContext() + ->negotiationMessage() + .isClientIdentityValue()); + + rc = rc_SUCCESS; + } + else { + errStream << "Unexpected event received: " << oldState << " -> " + << event; + BALL_LOG_ERROR << "#UNEXPECTED_STATE " << errStream.str() + << " [peer: " << channel()->peerUri() << "]"; + } + break; + } + case InitialConnectionEvent::e_ERROR: { + errStream << errorDescription; + } break; + case InitialConnectionEvent::e_NONE: { + errStream << "InitialConnectionContext: received e_NONE event"; + BALL_LOG_ERROR << "#UNEXPECTED_STATE " << errStream.str() + << " [peer: " << channel()->peerUri() << "]"; + break; + } + default: + errStream << "InitialConnectionContext: " + << "unexpected event received: " << event; + BALL_LOG_ERROR << "#UNEXPECTED_STATE " << errStream.str() + << " [peer: " << channel()->peerUri() << "]"; + } + + BALL_LOG_INFO << "In initial connection state transition: " << oldState + << " -> (" << event << ") -> " << d_state; + + bsl::shared_ptr session; + + if (rc == rc_SUCCESS && d_state == InitialConnectionState::e_NEGOTIATED) { + rc = d_negotiator_p->createSessionOnMsgType(errStream, &session, this); + BALL_LOG_INFO << "Created a session with " << channel()->peerUri(); + } + + if (rc != rc_SUCCESS) { + setState(InitialConnectionState::e_FAILED); + } + + if (hasFinalState()) { + BALL_LOG_INFO << "Finished initial connection with rc = " << rc + << ", error = '" << errStream.str() << "'"; + guard.release()->unlock(); + complete(rc, errStream.str(), session); + } +} + +// ACCESSORS bool InitialConnectionContext::isIncoming() const { return d_isIncoming; @@ -101,14 +735,16 @@ InitialConnectionContext::channel() const return d_channelSp; } -void InitialConnectionContext::complete( - int rc, - const bsl::string& error, - const bsl::shared_ptr& session) const +bmqp::EncodingType::Enum +InitialConnectionContext::authenticationEncodingType() const { - BSLS_ASSERT_SAFE(d_initialConnectionCompleteCb); + return d_authenticationEncodingType; +} - d_initialConnectionCompleteCb(rc, error, session, channel(), this); +const bsl::shared_ptr& +InitialConnectionContext::authenticationContext() const +{ + return d_authenticationCtxSp; } const bsl::shared_ptr& @@ -117,6 +753,27 @@ InitialConnectionContext::negotiationContext() const return d_negotiationCtxSp; } +InitialConnectionState::Enum InitialConnectionContext::state() const +{ + return d_state; +} + +bool InitialConnectionContext::hasFinalState() const +{ + return d_state == InitialConnectionState::e_FAILED || + d_state == InitialConnectionState::e_NEGOTIATED; +} + +void InitialConnectionContext::complete( + int rc, + const bsl::string& error, + const bsl::shared_ptr& session) const +{ + BSLS_ASSERT_SAFE(d_initialConnectionCompleteCb); + + d_initialConnectionCompleteCb(rc, error, session, channel(), this); +} + bool InitialConnectionContext::isClosed() const { return d_isClosed; diff --git a/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.h b/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.h index ad7236f29d..85d198dda3 100644 --- a/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.h +++ b/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.h @@ -17,20 +17,48 @@ #ifndef INCLUDED_MQBNET_INITIALCONNECTIONCONTEXT #define INCLUDED_MQBNET_INITIALCONNECTIONCONTEXT -//@PURPOSE: Provide a context for an initial connection handler. -// -//@CLASSES: -// mqbnet::InitialConnectionContext: VST for the context associated to -// an initial connection -// -//@DESCRIPTION: 'InitialConnectionContext' provides the context -// associated to an initial connection being established -// +/// @file mqbnet_initialconnectioncontext.h +/// @brief Context for authenticating and negotiating a new session. +/// +/// InitialConnectionContext owns the transient state needed while a +/// connection is performing the initial handshake (authentication followed +/// by negotiation, or direct negotiation with implicit/anonymous +/// authentication). +/// +/// Responsibilities: +/// - Hold caller‐supplied opaque pointers (user data / result state) so they +/// can flow between transport and application layers. +/// - Drive the read loop: schedule reads, accumulate bytes, decode initial +/// connection messages (Authentication / Negotiation), and dispatch them +/// as events to the FSM. +/// - Track the initial connection state via a finite state machine (FSM) that +/// handles authentication and negotiation transitions. +/// - Invoke the completion callback exactly once with either a fully +/// constructed Session or an error. +/// +/// A single instance is created per inbound or outbound connection attempt +/// and is discarded once the session is fully negotiated or the attempt +/// fails. + +// MQB +#include +#include +#include +#include + +// BMQ +#include +#include +#include // BDE +#include #include #include +#include #include +#include +#include namespace BloombergLP { @@ -45,20 +73,142 @@ namespace mqbnet { class SessionEventProcessor; class Cluster; class Session; +class AuthenticationContext; class NegotiationContext; +// ============================= +// struct InitialConnectionState +// ============================= + +struct InitialConnectionState { + // TYPES + enum Enum { + e_INITIAL = 0, // Initial state. + e_AUTHENTICATING = 1, // First message is authentication Request. + e_AUTHENTICATED = 2, // Authentication success. + e_ANON_AUTHENTICATING = 3, // First message is Negotiation Request. + e_NEGOTIATING_OUTBOUND = 4, // Outbound negotiation. + e_NEGOTIATED = 5, // Negotiation success. Final state. + e_FAILED = 6 // Final state. + }; + + // CLASS METHODS + + /// Write the string representation of the specified enumeration + /// `value` to the specified output `stream`, and return a reference to + /// `stream`. Optionally specify an initial indentation `level`, whose + /// absolute value is incremented recursively for nested objects. If + /// `level` is specified, optionally specify `spacesPerLevel`, whose + /// absolute value indicates the number of spaces per indentation level + /// for this and all of its nested objects. If `level` is negative, + /// suppress indentation of the first line. If `spacesPerLevel` is + /// negative, format the entire output on one line, suppressing all but + /// the initial indentation (as governed by `level`). See `toAscii` + /// for what constitutes the string representation of a + /// @bbref{InitialConnectionState::Enum} value. + static bsl::ostream& print(bsl::ostream& stream, + InitialConnectionState::Enum value, + int level = 0, + int spacesPerLevel = 4); + + /// Return the non-modifiable string representation corresponding to + /// the specified enumeration `value`, if it exists, and a unique + /// (error) string otherwise. The string representation of `value` + /// matches its corresponding enumerator name with the `e_` prefix + /// elided. Note that specifying a `value` that does not match any of + /// the enumerators will result in a string representation that is + /// distinct from any of those corresponding to the enumerators, but is + /// otherwise unspecified. + static const char* toAscii(InitialConnectionState::Enum value); + + /// Return true and fills the specified `out` with the enum value + /// corresponding to the specified `str`, if valid, or return false and + /// leave `out` untouched if `str` doesn't correspond to any value of + /// the enum. + static bool fromAscii(InitialConnectionState::Enum* out, + const bslstl::StringRef& str); +}; + +// FREE OPERATORS + +/// Format the specified `value` to the specified output `stream` and return +/// a reference to the modifiable `stream`. +bsl::ostream& operator<<(bsl::ostream& stream, + InitialConnectionState::Enum value); + +// ============================= +// struct InitialConnectionEvent +// ============================= + +struct InitialConnectionEvent { + // TYPES + enum Enum { + e_NONE = 0, + e_OUTBOUND_NEGOTATION = 1, + e_INCOMING = 2, + e_AUTHN_REQUEST = 3, + e_NEGOTIATION_MESSAGE = 4, + e_AUTHN_SUCCESS = 5, + e_ERROR = 6 + }; + + // CLASS METHODS + + /// Write the string representation of the specified enumeration + /// `value` to the specified output `stream`, and return a reference to + /// `stream`. Optionally specify an initial indentation `level`, whose + /// absolute value is incremented recursively for nested objects. If + /// `level` is specified, optionally specify `spacesPerLevel`, whose + /// absolute value indicates the number of spaces per indentation level + /// for this and all of its nested objects. If `level` is negative, + /// suppress indentation of the first line. If `spacesPerLevel` is + /// negative, format the entire output on one line, suppressing all but + /// the initial indentation (as governed by `level`). See `toAscii` + /// for what constitutes the string representation of a + /// @bbref{InitialConnectionEvent::Enum} value. + static bsl::ostream& print(bsl::ostream& stream, + InitialConnectionEvent::Enum value, + int level = 0, + int spacesPerLevel = 4); + + /// Return the non-modifiable string representation corresponding to + /// the specified enumeration `value`, if it exists, and a unique + /// (error) string otherwise. The string representation of `value` + /// matches its corresponding enumerator name with the `e_` prefix + /// elided. Note that specifying a `value` that does not match any of + /// the enumerators will result in a string representation that is + /// distinct from any of those corresponding to the enumerators, but is + /// otherwise unspecified. + static const char* toAscii(InitialConnectionEvent::Enum value); + + /// Return true and fills the specified `out` with the enum value + /// corresponding to the specified `str`, if valid, or return false and + /// leave `out` untouched if `str` doesn't correspond to any value of + /// the enum. + static bool fromAscii(InitialConnectionEvent::Enum* out, + const bslstl::StringRef& str); +}; + +// FREE OPERATORS + +/// Format the specified `value` to the specified output `stream` and return +/// a reference to the modifiable `stream`. +bsl::ostream& operator<<(bsl::ostream& stream, + InitialConnectionEvent::Enum value); + // ============================== // class InitialConnectionContext // ============================== -/// VST for the context associated to a session being negotiated. Each -/// session being negotiated get its own context; and the -/// InitialConnectionHandler concrete implementation can modify some of the -/// members during the handleInitialConnection() (i.e., between the -/// `handleInitialConnection()` method and the invocation of the -/// `InitialConnectionCompleteCb` method. -class InitialConnectionContext { +/// Each session being authenticated and negotiated get its own context. +class InitialConnectionContext +: public bsl::enable_shared_from_this { + private: + // CLASS-SCOPE CATEGORY + BALL_LOG_SET_CLASS_CATEGORY("MQBNET.INITIALCONNECTIONCONTEXT"); + public: + // TYPES typedef bsl::function d_channelSp; + /// The AuthenticationContext updated upon receiving an + /// authentication message. + bsl::shared_ptr d_authenticationCtxSp; + + /// The NegotiationContext updated upon receiving a negotiation message. + bsl::shared_ptr d_negotiationCtxSp; + /// The callback to invoke to notify of the status of the initial /// connection. InitialConnectionCompleteCb d_initialConnectionCompleteCb; - /// The NegotiationContext updated upon receiving a negotiation message. - bsl::shared_ptr d_negotiationCtxSp; + /// Encoding for authentication messages. Defaults to BER until the first + /// inbound authentication message is decoded, then set to that message's + /// encoding and reused for outbound replies. Temporary field; copied into + /// the AuthenticationContext later. + bmqp::EncodingType::Enum d_authenticationEncodingType; - /// True if the session being negotiated originates - /// from a remote peer (i.e., a 'listen'); false if - /// it originates from us (i.e., a 'connect). + /// The state of the initial connection. + InitialConnectionState::Enum d_state; + + /// True if the session being negotiated originates from a remote peer + /// (i.e., a 'listen'); false if it originates from us (i.e., a 'connect'). bool d_isIncoming; /// True if the associated channel is closed (with `onClose`). bool d_isClosed; + private: + // NOT IMPLEMENTED + + /// Copy constructor and assignment operator are not implemented. + InitialConnectionContext(const InitialConnectionContext&); // = delete; + InitialConnectionContext& + operator=(const InitialConnectionContext&); // = delete; + public: + // TRAITS + BSLMF_NESTED_TRAIT_DECLARATION(InitialConnectionContext, + bslma::UsesBslmaAllocator) + // CREATORS /// Create a new object having the specified `isIncoming` value. - explicit InitialConnectionContext(bool isIncoming); + InitialConnectionContext( + bool isIncoming, + mqbnet::Authenticator* authenticator, + mqbnet::Negotiator* negotiator, + void* userData, + void* resultState, + const bsl::shared_ptr& channel, + const InitialConnectionCompleteCb& initialConnectionCompleteCb, + bslma::Allocator* allocator = 0); ~InitialConnectionContext(); + private: + // PRIVATE MANIPULATORS + void setState(InitialConnectionState::Enum value); + + /// Schedule the next read operation on the channel. + /// Return 0 on success, or a non-zero error code and populate + /// `errorDescription` with details on failure. + int scheduleRead(bsl::ostream& errorDescription); + + /// Read from the channel into the specified `outPacket`. On success, + /// return 0, set `isFullBlob` to indicate whether a full message has been + /// read, and set `numNeeded` to the number of additional bytes needed for + /// a full message (0 if `isFullBlob` is true). On error, return a + /// non-zero code and populate the specified `errorDescription` with a + /// description of the error. + int readBlob(bsl::ostream& errorDescription, + bdlbb::Blob* outPacket, + bool* isFullBlob, + int* numNeeded, + bdlbb::Blob* blob); + + /// Decode the specified `blob` received from the channel and handle it + /// based on the type of the message received. On success, return 0. On + /// error, return a non-zero code and populate the specified + /// `errorDescription` with a description of the error. + int processBlob(bsl::ostream& errorDescription, const bdlbb::Blob& blob); + + /// Decode the initial connection messages received in the specified + /// `blob` and store it, on success, in the specified `message`, returning + /// 0. Return a non-zero code on error and populate the specified + /// `errorDescription` with a description of the error. + int decodeInitialConnectionMessage( + bsl::ostream& errorDescription, + bsl::variant* message, + const bdlbb::Blob& blob); + + /// Create and initialize a `NegotiationContext`. + void createNegotiationContext(); + + /// Perform anonymous authentication using the anonymous credential for the + /// current context. Return a non-zero code on error and + /// populate the specified `errorDescription` with a description of the + /// error. + int handleAnonAuthentication(bsl::ostream& errorDescription); + + public: // MANIPULATORS - /// Set the corresponding field to the specified `value` and return a - /// reference offering modifiable access to this object. - InitialConnectionContext& setUserData(void* value); - InitialConnectionContext& setResultState(void* value); - InitialConnectionContext& - setChannel(const bsl::shared_ptr& value); - InitialConnectionContext& - setCompleteCb(const InitialConnectionCompleteCb& value); - InitialConnectionContext& + /// Set the corresponding field to the specified `value`. + void setResultState(void* value); + void setAuthenticationContext( + const bsl::shared_ptr& value); + void setNegotiationContext(const bsl::shared_ptr& value); - /// Called by the IO upon `onCLose` signal + /// Called by the IO upon `onClose` signal void onClose(); + /// Read callback invoked when data is available on the channel. + /// Process the received `blob` if `status` indicates success. + /// Set `numNeeded` to request additional bytes if needed for a + /// full message. + void readCallback(const bmqio::Status& status, + int* numNeeded, + bdlbb::Blob* blob); + + /// Entrance to the initial connection process. + void handleInitialConnection(); + + /// Process an InitialConnectionEvent `event` with the given + /// `errorDescription` and drive the authentication/negotiation + /// state machine. The `message` optionally contains any associated + /// authentication/negotiation message data. + void handleEvent(const bsl::string& errorDescription, + InitialConnectionEvent::Enum event, + const bsl::variant& + message = bsl::monostate()); + // ACCESSORS /// Return the value of the corresponding field. - bool isIncoming() const; - void* userData() const; - void* resultState() const; - const bsl::shared_ptr& channel() const; + bool isIncoming() const; + void* userData() const; + void* resultState() const; + const bsl::shared_ptr& channel() const; + bmqp::EncodingType::Enum authenticationEncodingType() const; + const bsl::shared_ptr& + authenticationContext() const; const bsl::shared_ptr& negotiationContext() const; bool isClosed() const; + InitialConnectionState::Enum state() const; + bool hasFinalState() const; + /// Invoke the `initialConnectionCompleteCb` callback with the specified + /// return code `rc`, `error` description, and `session` (negotiated + /// session or empty if there's any failure). void complete(int rc, const bsl::string& error, const bsl::shared_ptr& session) const; }; } // close package namespace + +// ----------------------------- +// struct InitialConnectionState +// ----------------------------- + +// FREE OPERATORS +inline bsl::ostream& +mqbnet::operator<<(bsl::ostream& stream, + mqbnet::InitialConnectionState::Enum value) +{ + return InitialConnectionState::print(stream, value, 0, -1); +} + +// ----------------------------- +// struct InitialConnectionEvent +// ----------------------------- + +// FREE OPERATORS +inline bsl::ostream& +mqbnet::operator<<(bsl::ostream& stream, + mqbnet::InitialConnectionEvent::Enum value) +{ + return InitialConnectionEvent::print(stream, value, 0, -1); +} + } // close enterprise namespace #endif diff --git a/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.t.cpp b/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.t.cpp deleted file mode 100644 index c91849bac9..0000000000 --- a/src/groups/mqb/mqbnet/mqbnet_initialconnectioncontext.t.cpp +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2023 Bloomberg Finance L.P. -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// mqbnet_initialconnectioncontext.t.cpp -*-C++-*- -#include - -// MQB -#include -#include -#include - -// BMQ -#include - -// BDE -#include -#include - -// TEST DRIVER -#include - -// CONVENIENCE -using namespace BloombergLP; -using namespace bsl; - -namespace { - -void complete(const bsl::shared_ptr& check, - int status, - const bsl::string& errorDescription, - const bsl::shared_ptr& session, - const bsl::shared_ptr& channel, - const mqbnet::InitialConnectionContext* initialConnectionContext) -{ - BSLS_ASSERT_SAFE(check); - - BSLS_ASSERT_SAFE(*check == 0); - - *check = status; - - (void)errorDescription; - (void)session; - (void)channel; - (void)initialConnectionContext; -} - -} // close unnamed namespace - -// ============================================================================ -// TESTS -// ---------------------------------------------------------------------------- - -static void test1_initialConnectionContext() -{ - bmqtst::TestHelper::printTestName("test1_initialConnectionContext"); - { - PV("Constructor"); - mqbnet::InitialConnectionContext obj1(true); - BMQTST_ASSERT_EQ(obj1.isIncoming(), true); - BMQTST_ASSERT_EQ(obj1.resultState(), static_cast(0)); - BMQTST_ASSERT_EQ(obj1.userData(), static_cast(0)); - - mqbnet::InitialConnectionContext obj2(false); - BMQTST_ASSERT_EQ(obj2.isIncoming(), false); - } - - { - PV("Manipulators/Accessors"); - - mqbnet::InitialConnectionContext obj(true); - - { // UserData - int value = 7; - BMQTST_ASSERT_EQ(&(obj.setUserData(&value)), &obj); - BMQTST_ASSERT_EQ(obj.userData(), &value); - } - - { // ResultState - int value = 9; - BMQTST_ASSERT_EQ(&(obj.setResultState(&value)), &obj); - BMQTST_ASSERT_EQ(obj.resultState(), &value); - } - - { // Channel - bsl::shared_ptr channel; - bslma::Allocator* allocator = bmqtst::TestHelperUtil::allocator(); - channel.createInplace(allocator); - BMQTST_ASSERT_EQ(&(obj.setChannel(channel)), &obj); - BMQTST_ASSERT_EQ(obj.channel(), channel); - } - - { - // CompletionCb - - bsl::shared_ptr check; - int rc = 1; - bslma::Allocator* allocator = bmqtst::TestHelperUtil::allocator(); - - check.createInplace(allocator, 0); - - obj.setCompleteCb(bdlf::BindUtil::bind( - &complete, - check, - bdlf::PlaceHolders::_1, // status - bdlf::PlaceHolders::_2, // errorDescription - bdlf::PlaceHolders::_3, // session - bdlf::PlaceHolders::_4, // channel - bdlf::PlaceHolders::_5 // initialConnectionContext - )); - obj.complete(rc, - bsl::string(), - bsl::shared_ptr()); - - BMQTST_ASSERT_EQ(*check, rc); - } - } -} - -// ============================================================================ -// MAIN PROGRAM -// ---------------------------------------------------------------------------- - -int main(int argc, char* argv[]) -{ - TEST_PROLOG(bmqtst::TestHelper::e_DEFAULT); - - switch (_testCase) { - case 0: - case 1: test1_initialConnectionContext(); break; - default: { - cerr << "WARNING: CASE '" << _testCase << "' NOT FOUND." << endl; - bmqtst::TestHelperUtil::testStatus() = -1; - } break; - } - - TEST_EPILOG(bmqtst::TestHelper::e_CHECK_GBL_ALLOC); -} diff --git a/src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.h b/src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.h deleted file mode 100644 index f3d285aabc..0000000000 --- a/src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.h +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2025 Bloomberg Finance L.P. -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// mqbnet_initialconnectionhandler.h -*-C++-*- -#ifndef INCLUDED_MQBNET_INITIALCONNECTIONHANDLER -#define INCLUDED_MQBNET_INITIALCONNECTIONHANDLER - -//@PURPOSE: -// -//@CLASSES: -// -//@DESCRIPTION: Read from IO and commands authenticator and negotiator. -// A session would be created at the end upon success. - -// MQB -#include - -// BDE -#include - -namespace BloombergLP { - -// FORWARD DECLARATION -namespace bmqio { -class Channel; -} - -namespace mqbnet { - -// ============================== -// class InitialConnectionHandler -// ============================== - -class InitialConnectionHandler { - public: - // TYPES - typedef bsl::shared_ptr - InitialConnectionContextSp; - - public: - // CREATORS - - /// Destructor - virtual ~InitialConnectionHandler(); - - // MANIPULATORS - - /// Method invoked by the client of this object to negotiate a session. - /// The specified `context` is an in-out member holding the initial - /// connection context to use, including an `InitialConnectionCompleteCb`, - /// which must be called with the result, whether success or failure, of - /// the initial connection. - /// The InitialConnectionHandler concrete implementation can modify some of - /// the members during the initial connection (i.e., between the - /// `handleInitialConnection()` method and the invocation of the - /// `InitialConnectionCompleteCb` method. Note that if no initial - /// connection is needed, the `InitialConnectionCompleteCb` may be invoked - /// directly from inside the call to `handleInitialConnection()`. - virtual void - handleInitialConnection(const InitialConnectionContextSp& context) = 0; -}; - -} // close package namespace -} // close enterprise namespace - -#endif diff --git a/src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.t.cpp b/src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.t.cpp deleted file mode 100644 index dea64c78cc..0000000000 --- a/src/groups/mqb/mqbnet/mqbnet_initialconnectionhandler.t.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2023 Bloomberg Finance L.P. -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// mqbnet_initialconnectionhandler.t.cpp -*-C++-*- -#include - -// MQB -#include -#include -#include - -// BMQ -#include - -// BDE -#include -#include -#include - -// TEST DRIVER -#include - -// CONVENIENCE -using namespace BloombergLP; -using namespace bsl; - -// ============================================================================ -// HELPER CLASSES AND FUNCTIONS FOR TESTING -// ---------------------------------------------------------------------------- - -/// A test implementation of the `mqbnet::InitialConnectionHandler` protocol -struct InitialConnectionHandlerTestImp -: bsls::ProtocolTestImp { - void handleInitialConnection(const InitialConnectionContextSp& context) - BSLS_KEYWORD_OVERRIDE; -}; - -void InitialConnectionHandlerTestImp::handleInitialConnection( - BSLA_UNUSED const InitialConnectionContextSp& context) -{ - markDone(); -} - -// ============================================================================ -// TESTS -// ---------------------------------------------------------------------------- - -static void test1_InitialConnectionHandler() -// ------------------------------------------------------------------------ -// PROTOCOL TEST: -// Ensure this class is a properly defined protocol. -// -// Concerns: -//: 1 The protocol is abstract: no objects of it can be created. -//: -//: 2 The protocol has no data members. -//: -//: 3 The protocol has a virtual destructor. -//: -//: 4 All methods of the protocol are pure virtual. -//: -//: 5 All methods of the protocol are publicly accessible. -// -// Plan: -//: 1 Define a concrete derived implementation, -//: 'InitialConnectionHandlerTestImp', of the -//: protocol. -//: -//: 2 Create an object of the 'bsls::ProtocolTest' class template -//: parameterized by 'InitialConnectionHandlerTestImp', and use it to verify -//: that: -//: -//: 1 The protocol is abstract. (C-1) -//: -//: 2 The protocol has no data members. (C-2) -//: -//: 3 The protocol has a virtual destructor. (C-3) -//: -//: 3 Use the 'BSLS_PROTOCOLTEST_ASSERT' macro to verify that -//: non-creator methods of the protocol are: -//: -//: 1 virtual, (C-4) -//: -//: 2 publicly accessible. (C-5) -// -// Testing: -// PROTOCOL TEST -// ------------------------------------------------------------------------ -{ - bmqtst::TestHelper::printTestName("InitialConnectionHandler"); - - PV("Creating a test object"); - bsls::ProtocolTest testObj( - bmqtst::TestHelperUtil::verbosityLevel() > 2); - - PV("Verify that the protocol is abstract"); - BMQTST_ASSERT(testObj.testAbstract()); - - PV("Verify that there are no data members"); - BMQTST_ASSERT(testObj.testNoDataMembers()); - - PV("Verify that the destructor is virtual"); - BMQTST_ASSERT(testObj.testVirtualDestructor()); - - { - PV("Verify that methods are public and virtual"); - - mqbnet::InitialConnectionHandler::InitialConnectionContextSp - dummyContext_sp; - - BSLS_PROTOCOLTEST_ASSERT(testObj, - handleInitialConnection(dummyContext_sp)); - } -} - -// ============================================================================ -// MAIN PROGRAM -// ---------------------------------------------------------------------------- - -int main(int argc, char* argv[]) -{ - TEST_PROLOG(bmqtst::TestHelper::e_DEFAULT); - - switch (_testCase) { - case 0: - case 1: test1_InitialConnectionHandler(); break; - default: { - cerr << "WARNING: CASE '" << _testCase << "' NOT FOUND." << endl; - bmqtst::TestHelperUtil::testStatus() = -1; - } break; - } - - TEST_EPILOG(bmqtst::TestHelper::e_CHECK_DEF_GBL_ALLOC); -} diff --git a/src/groups/mqb/mqbnet/mqbnet_negotiationcontext.h b/src/groups/mqb/mqbnet/mqbnet_negotiationcontext.h index 6e6c21e709..ede3c67967 100644 --- a/src/groups/mqb/mqbnet/mqbnet_negotiationcontext.h +++ b/src/groups/mqb/mqbnet/mqbnet_negotiationcontext.h @@ -67,8 +67,8 @@ class NegotiationContext { /// The event processor to use for initiating the /// read on the channel once the session has been /// successfully negotiated. This may or may not be - /// set by the caller, before invoking - /// 'InitialConnectionHandler::handleInitialConnection()'; + /// set by the caller, during + /// 'TcpSessionFactory::handleInitialConnection()'; /// and may or may not be changed by the negotiator concrete /// implementation before invoking the /// 'InitialConnectionCompleteCb'. Note that a value of 0 will diff --git a/src/groups/mqb/mqbnet/mqbnet_tcpsessionfactory.cpp b/src/groups/mqb/mqbnet/mqbnet_tcpsessionfactory.cpp index 98ac90238a..9baaea9867 100644 --- a/src/groups/mqb/mqbnet/mqbnet_tcpsessionfactory.cpp +++ b/src/groups/mqb/mqbnet/mqbnet_tcpsessionfactory.cpp @@ -23,7 +23,7 @@ /// in order, regardless of the success or failure of the negotiation: /// - `channelStateCallback` /// - `negotiate` -/// - `negotiationComplete` +/// - `initialConnectionComplete` /// /// When a channel goes down, `onClose()` is the only method being invoked. @@ -31,7 +31,10 @@ #include #include #include +#include +#include #include +#include #include #include @@ -342,46 +345,69 @@ void TCPSessionFactory::handleInitialConnection( { // executed by one of the *IO* threads + enum RcEnum { + // Value for the various RC error categories + rc_SUCCESS = 0, + }; + BALL_LOG_INFO << "TCPSessionFactory '" << d_config.name() << "': allocating a channel with '" << channel.get() << "' [" << d_nbActiveChannels << " active channels]"; // Create a unique InitialConnectionContext for the channel, from - // the OperationContext. This shared_ptr is bound to the - // 'negotiationComplete' callback below, which is what scopes its lifetime. - bsl::shared_ptr initialConnectionContext; - initialConnectionContext.createInplace(d_allocator_p, - context->d_isIncoming); - (*initialConnectionContext) - .setUserData(context->d_negotiationUserData_sp.get()) - .setResultState(context->d_resultState_p) - .setChannel(channel) - .setCompleteCb(bdlf::BindUtil::bind( - &TCPSessionFactory::negotiationComplete, - this, - bdlf::PlaceHolders::_1, // status - bdlf::PlaceHolders::_2, // errorDescription - bdlf::PlaceHolders::_3, // session - bdlf::PlaceHolders::_4, // channel - bdlf::PlaceHolders::_5, // initialConnectionContext - context)); + // the OperationContext. When we start reading incoming messages, this + // shared_ptr will be bound to the callback function of the ntc reader. + // Since `handleEvent()` is always called as a result of reading, and + // `InitialConnectionCompleteCb` is always triggered at the end of + // `handleEvent()`, its lifetime ends when `InitialConnectionCompleteCb` + // finishes. + + bsl::shared_ptr initialConnectionContext = + bsl::allocate_shared( + d_allocator_p, + context->d_isIncoming, + d_authenticator_p, + d_negotiator_p, + context->d_negotiationUserData_sp.get(), + context->d_resultState_p, + channel, + bdlf::BindUtil::bindS( + d_allocator_p, + &TCPSessionFactory::initialConnectionComplete, + this, + bdlf::PlaceHolders::_1, // status + bdlf::PlaceHolders::_2, // errorDescription + bdlf::PlaceHolders::_3, // session + bdlf::PlaceHolders::_4, // channel + bdlf::PlaceHolders::_5, // initialConnectionContext + context)); + + // Cache the context. It will be removed in 'initialConnectionComplete'. + d_initialConnectionContextCache[initialConnectionContext.get()] = + initialConnectionContext; // Register as observer of the channel to get the 'onClose' channel->onClose( bdlf::BindUtil::bindS(d_allocator_p, &TCPSessionFactory::onClose, this, - initialConnectionContext, + initialConnectionContext.get(), + channel, bdlf::PlaceHolders::_1 /* bmqio::Status */)); - // NOTE: we must ensure the 'initialConnectionCompleteCb' can be invoked - // from the - // 'handleInitialConnection()' call as specified on the - // 'InitialConnectionHandler::handleInitialConnection' method - // contract (this means we can't have mutex lock around the call to - // 'handleInitialConnection'). - d_initialConnectionHandler_p->handleInitialConnection( - initialConnectionContext); + if (!initialConnectionContext->isIncoming()) { + // TODO: When we are ready to move on to the next step, we should + // call `authenticationOutbound` here instead before calling + // `negotiateOutbound`. + initialConnectionContext->handleEvent( + bsl::string(), + mqbnet::InitialConnectionEvent::e_OUTBOUND_NEGOTATION); + } + else { + initialConnectionContext->handleEvent( + bsl::string(), + mqbnet::InitialConnectionEvent::e_INCOMING); + } } void TCPSessionFactory::readCallback(const bmqio::Status& status, @@ -488,14 +514,19 @@ void TCPSessionFactory::readCallback(const bmqio::Status& status, if (channelInfo->d_monitor.checkData(channelInfo->d_channel_sp.get(), event)) { - channelInfo->d_eventProcessor_p->processEvent( - event, - channelInfo->d_session_sp->clusterNode()); + if (event.isAuthenticationEvent()) { + reauthnOnAuthenticationEvent(event, channelInfo); + } + else { + channelInfo->d_eventProcessor_p->processEvent( + event, + channelInfo->d_session_sp->clusterNode()); + } } } } -void TCPSessionFactory::negotiationComplete( +void TCPSessionFactory::initialConnectionComplete( int statusCode, const bsl::string& errorDescription, const bsl::shared_ptr& session, @@ -503,19 +534,31 @@ void TCPSessionFactory::negotiationComplete( const InitialConnectionContext* initialConnectionContext_p, const bsl::shared_ptr& operationContext) { - // executed by one of the *IO* threads + // executed by one of the *IO* threads or an *AUTHENTICATION* thread + + // Reset any authentication message stored in the authentication context + if (initialConnectionContext_p->authenticationContext()) { + initialConnectionContext_p->authenticationContext() + ->resetAuthenticationMessage(); + } if (statusCode != 0) { // Failed to negotiate - BALL_LOG_WARN << "#SESSION_NEGOTIATION " - << "TCPSessionFactory '" << d_config.name() << "' " - << "failed to negotiate a session " + BALL_LOG_WARN << "#INITIAL_CONNECTION TCPSessionFactory '" + << d_config.name() << "' " + << "failed to authenticate/negotiate a session " << "[channel: '" << channel.get() << "', status: " << statusCode << ", error: '" << errorDescription << "']"; + // Remove from cache before closing channel + { + bslmt::LockGuard guard(&d_mutex); + d_initialConnectionContextCache.erase(initialConnectionContext_p); + } + bmqio::Status status(bmqio::StatusCategory::e_GENERIC_ERROR, - "negotiationError", + "initialconnectionError", statusCode, d_allocator_p); channel->close(status); @@ -527,17 +570,22 @@ void TCPSessionFactory::negotiationComplete( return; // RETURN } + BSLS_ASSERT_SAFE( + d_initialConnectionContextCache.contains(initialConnectionContext_p)); + bsl::shared_ptr initialConnectionContext_sp = + d_initialConnectionContextCache.at(initialConnectionContext_p); + // Successful negotiation - BSLS_ASSERT_SAFE(initialConnectionContext_p); BSLS_ASSERT_SAFE(initialConnectionContext_p->negotiationContext()); - BALL_LOG_INFO << "TCPSessionFactory '" << d_config.name() - << "' successfully negotiated a session [session: '" - << session->description() << "', channel: '" << channel.get() - << "', maxMissedHeartbeat: " - << initialConnectionContext_p->negotiationContext() - ->maxMissedHeartbeats() - << "]"; + BALL_LOG_INFO + << "TCPSessionFactory '" << d_config.name() + << "' successfully authenticated and negotiated a session [session: '" + << session->description() << "', channel: '" << channel.get() + << "', maxMissedHeartbeat: " + << initialConnectionContext_p->negotiationContext() + ->maxMissedHeartbeats() + << "]"; // Session is established; keep a hold to it. @@ -564,6 +612,9 @@ void TCPSessionFactory::negotiationComplete( { bslmt::LockGuard guard(&d_mutex); // LOCK + // Remove the cached InitialConnectionContext under lock + d_initialConnectionContextCache.erase(initialConnectionContext_p); + ++d_nbSessions; if (isClientOrProxy(session.get())) { @@ -571,8 +622,7 @@ void TCPSessionFactory::negotiationComplete( } // check if the channel is not closed (we can be in authentication - // thread) - + // thread while the channel is closed in IO thread) if (initialConnectionContext_p->isClosed()) { BALL_LOG_WARN << "#TCP_UNEXPECTED_STATE TCPSessionFactory '" @@ -587,6 +637,7 @@ void TCPSessionFactory::negotiationComplete( info.createInplace( d_allocator_p, channel, + initialConnectionContext_p->authenticationContext(), monitoredSession, initialConnectionContext_p->negotiationContext()->eventProcessor(), initialConnectionContext_p->negotiationContext() @@ -757,17 +808,14 @@ void TCPSessionFactory::channelStateCallback( } } -void TCPSessionFactory::onClose( - const bsl::shared_ptr& initialConnectionContext, - const bmqio::Status& status) +void TCPSessionFactory::onClose(const InitialConnectionContext* context_p, + const bsl::shared_ptr& channel, + const bmqio::Status& status) { - // Executed by one of the IO threads. + // Executed by *ANY* thread --d_nbActiveChannels; - const bsl::shared_ptr& channel = - initialConnectionContext->channel(); - int port; channel->properties().load( &port, @@ -780,7 +828,12 @@ void TCPSessionFactory::onClose( // set the 'isClosed' flag under lock to be checked under lock in // 'negotiationComplete'. - initialConnectionContext->onClose(); + bsl::shared_ptr initialConnectionContext_sp; + if (d_initialConnectionContextCache.contains(context_p)) { + initialConnectionContext_sp = d_initialConnectionContextCache.at( + context_p); + initialConnectionContext_sp->onClose(); + } ChannelMap::const_iterator it = d_channels.find(channel.get()); if (it != d_channels.end()) { @@ -827,6 +880,11 @@ void TCPSessionFactory::onClose( << d_nbActiveChannels << " active channels" << ", status: " << status << "]"; + // Disable reauthentication timer if there's any + if (channelInfo->d_authenticationCtx_sp) { + channelInfo->d_authenticationCtx_sp->onClose(d_scheduler_p); + } + // TearDown the session int isBrokerShutdown = false; if (status.category() == bmqio::StatusCategory::e_SUCCESS) { @@ -935,11 +993,71 @@ void TCPSessionFactory::stopHeartbeats() d_heartbeatChannels.clear(); } +int TCPSessionFactory::validateTcpInterfaces() const +{ + mqbcfg::TcpInterfaceConfigValidator validator; + return validator(d_config); +} + +void TCPSessionFactory::reauthnOnAuthenticationEvent( + const bmqp::Event& event, + const ChannelInfo* channelInfo) const +{ + // executed by the *IO* thread + + // PRECONDITIONS + BSLS_ASSERT_SAFE(channelInfo); + BSLS_ASSERT_SAFE(channelInfo->d_authenticationCtx_sp); + BSLS_ASSERT_SAFE(channelInfo->d_session_sp); + + const bsl::shared_ptr& context = + channelInfo->d_authenticationCtx_sp; + const bsl::string& description = channelInfo->d_session_sp->description(); + + if (!context->tryStartReauthentication()) { + BALL_LOG_ERROR << "#CLIENT_IMPROPER_BEHAVIOR " << description + << ": Dropping Authentication event since " + "authentication is in progress: " + << event; + return; // RETURN + } + + bmqp_ctrlmsg::AuthenticationMessage authenticationMessage; + int rc = event.loadAuthenticationEvent(&authenticationMessage); + if (rc != 0) { + BALL_LOG_ERROR << "#CORRUPTED_EVENT " << description + << ": Received invalid authentication message " + "from client [reason: 'failed to decode', rc: " + << rc << "]:\n" + << bmqu::BlobStartHexDumper(event.blob()); + return; // RETURN + } + + BALL_LOG_INFO << description << ": Received an authentication message"; + + context->setAuthenticationMessage(authenticationMessage); + context->setAuthenticationEncodingType( + event.authenticationEventEncodingType()); + + bmqu::MemOutStream errorStream; + + rc = d_authenticator_p->handleReauthentication(errorStream, + context, + channelInfo->d_channel_sp); + if (rc != 0) { + BALL_LOG_ERROR << "#AUTHENTICATION_FAILED " << description + << ": Authentication failed [reason: '" + << errorStream.str() << "', rc: " << rc << "]"; + return; // RETURN + } +} + TCPSessionFactory::TCPSessionFactory( const mqbcfg::TcpInterfaceConfig& config, bdlmt::EventScheduler* scheduler, bdlbb::BlobBufferFactory* blobBufferFactory, - InitialConnectionHandler* initialConnectionHandler, + Authenticator* authenticator, + Negotiator* negotiator, mqbstat::StatController* statController, bslma::Allocator* allocator) : d_self(this) // use default allocator @@ -947,7 +1065,8 @@ TCPSessionFactory::TCPSessionFactory( , d_config(config, allocator) , d_scheduler_p(scheduler) , d_blobBufferFactory_p(blobBufferFactory) -, d_initialConnectionHandler_p(initialConnectionHandler) +, d_authenticator_p(authenticator) +, d_negotiator_p(negotiator) , d_statController_p(statController) , d_tcpChannelFactory_mp() , d_resolutionContext(allocator) @@ -1015,12 +1134,6 @@ TCPSessionFactory::~TCPSessionFactory() d_self.invalidate(); } -int TCPSessionFactory::validateTcpInterfaces() const -{ - mqbcfg::TcpInterfaceConfigValidator validator; - return validator(d_config); -} - void TCPSessionFactory::cancelListeners() { for (ListeningHandleMap::iterator it = d_listeningHandles.begin(), @@ -1552,12 +1665,14 @@ bool TCPSessionFactory::isEndpointLoopback(const bslstl::StringRef& uri) const // ------------------------------------ TCPSessionFactory::ChannelInfo::ChannelInfo( - const bsl::shared_ptr& channel_sp, - const bsl::shared_ptr& monitoredSession, - SessionEventProcessor* eventProcessor, - int maxMissedHeartbeats, - int initialMissedHeartbeatCounter) + const bsl::shared_ptr& channel_sp, + const bsl::shared_ptr& authenticationContext, + const bsl::shared_ptr& monitoredSession, + SessionEventProcessor* eventProcessor, + int maxMissedHeartbeats, + int initialMissedHeartbeatCounter) : d_channel_sp(channel_sp) +, d_authenticationCtx_sp(authenticationContext) , d_session_sp(monitoredSession) , d_eventProcessor_p(eventProcessor) , d_monitor(maxMissedHeartbeats, initialMissedHeartbeatCounter) diff --git a/src/groups/mqb/mqbnet/mqbnet_tcpsessionfactory.h b/src/groups/mqb/mqbnet/mqbnet_tcpsessionfactory.h index b9cc7bf8ea..b9801c6970 100644 --- a/src/groups/mqb/mqbnet/mqbnet_tcpsessionfactory.h +++ b/src/groups/mqb/mqbnet/mqbnet_tcpsessionfactory.h @@ -78,9 +78,8 @@ // stale connection will be dropped after a time of ']12;16]' seconds. // MQB - #include -#include +#include #include #include @@ -122,6 +121,8 @@ class Session; class SessionEventProcessor; class TCPSessionFactoryIterator; struct TCPSessionFactory_OperationContext; +class Authenticator; +class Negotiator; // ======================= // class TCPSessionFactory @@ -182,6 +183,9 @@ class TCPSessionFactory { /// The channel bsl::shared_ptr d_channel_sp; + // The context of authentication + bsl::shared_ptr d_authenticationCtx_sp; + /// The session tied to the channel bsl::shared_ptr d_session_sp; @@ -191,6 +195,8 @@ class TCPSessionFactory { bmqp::HeartbeatMonitor d_monitor; /// @param channel_sp The channel + /// @param authenticationContext The authentication context associated + /// with this channel. /// @param session The session associated with this channel. /// @param eventProcessor The event processor of Events received on /// this channel. @@ -199,9 +205,11 @@ class TCPSessionFactory { /// @param initialMissedHeartbeatCounter The initial missed heartbeats /// for this channel. explicit ChannelInfo(const bsl::shared_ptr& channel_sp, - const bsl::shared_ptr& session, - SessionEventProcessor* eventProcessor, - int maxMissedHeartbeats, + const bsl::shared_ptr& + authenticationContext, + const bsl::shared_ptr& session, + SessionEventProcessor* eventProcessor, + int maxMissedHeartbeats, int initialMissedHeartbeatCounter); }; @@ -261,6 +269,10 @@ class TCPSessionFactory { typedef bslma::ManagedPtr StatChannelFactoryMp; + typedef bsl::unordered_map > + InitialConnectionContextMp; + typedef TCPSessionFactory_OperationContext OperationContext; typedef bsl::shared_ptr OpHandleSp; @@ -288,9 +300,11 @@ class TCPSessionFactory { /// BlobBuffer factory to use (passed to the ChannelFactory) bdlbb::BlobBufferFactory* d_blobBufferFactory_p; - /// Initial Connection Handler to use for orchestraing - /// authentication and negotiation - InitialConnectionHandler* d_initialConnectionHandler_p; + /// Authenticator to use for authentication + Authenticator* d_authenticator_p; + + /// Negotiator to use for negotiation + Negotiator* d_negotiator_p; /// Channels' stat context (passed to TCPSessionFactory) mqbstat::StatController* d_statController_p; @@ -307,6 +321,14 @@ class TCPSessionFactory { StatChannelFactoryMp d_statChannelFactory_mp; + /// Cache of shared pointers to @bbref{mqbnet::InitialConnectionContext} to + /// preserve their lifetime while an initial connection + /// (authentication/negotiation) is in progress. Each context is added by + /// @bbref{handleInitialConnection} and removed by + /// @bbref{initialConnectionComplete} once negotiation finishes (success or + /// failure). + InitialConnectionContextMp d_initialConnectionContextCache; + /// Name to use for the IO threads bsl::string d_threadName; @@ -391,8 +413,6 @@ class TCPSessionFactory { handleInitialConnection(const bsl::shared_ptr& channel, const bsl::shared_ptr& context); - // PRIVATE MANIPULATORS - /// Process a protocol packet received from the specified `channel` with /// the associated specified `channelInfo`. If the specified `status` /// is 0, this indicates data is available in the specified `blob`; @@ -407,21 +427,20 @@ class TCPSessionFactory { bdlbb::Blob* blob, ChannelInfo* channelInfo); - /// Method invoked when the negotiation of the specified `channel` is - /// complete, whether it be success or failure. The specified - /// `context` is the `OperationContext` struct created during the - /// listen or connect call that is responsible for this negotiation (and - /// hence, in the case of `listen`, is common for all sessions - /// negotiated); while the specified `negotiatorContext` corresponds to - /// the unique context passed in to the `negotiate` method of the - /// Negotiator, for that `channel`. If the specified `statusCode` is 0, - /// the negotiation was a success and the specified `session` contains - /// the negotiated session. If `status` is non-zero, the negotiation - /// was a failure and `session` will be null, with the specified - /// `errorDescription` containing a description of the error. In either - /// case, the specified `callback` must be invoked to notify the channel - /// factory of the status. - void negotiationComplete( + /// Method invoked when the initial connection (including authentication + /// and negotiation) of the specified `channel` is complete, whether it be + /// success or failure. The specified `userData` is the `OperationContext` + /// struct created during the listen or connect call that is responsible + /// for this negotiation (and hence, in the case of `listen`, is common for + /// all sessions negotiated); while the specified + /// `initialConnectionContext_p` corresponds to the unique context created + /// during `handleInitialConnection` method for that `channel`. If the + /// specified `statusCode` is 0, the initial connection was a success and + /// the specified `session` contains the negotiated session. If `status` + /// is non-zero, the initial connection was a failure and `session` will be + /// null, with the specified `errorDescription` containing a description of + /// the error. + void initialConnectionComplete( int statusCode, const bsl::string& errorDescription, const bsl::shared_ptr& session, @@ -454,12 +473,11 @@ class TCPSessionFactory { /// Method invoked by the channel factory to notify that the specified /// `channel` went down, with the specified `status` corresponding to - /// the channel's status, `userData` corresponding to the one provided - /// when calling `addObserver` to register this object as observer of - /// the channel. - virtual void onClose(const bsl::shared_ptr& - initialConnectionContext, - const bmqio::Status& status); + /// the channel's status. The specified `context_p` is used to track + /// whether the channel is closed. + void onClose(const InitialConnectionContext* context_p, + const bsl::shared_ptr& channel, + const bmqio::Status& status); /// Reccuring scheduler event to check for all `heartbeat-enabled` /// channels : this will send a heartbeat if no data has been received @@ -481,6 +499,15 @@ class TCPSessionFactory { /// timestamps map. void logOpenSessionTime(const bsl::string& sessionDescription, const bsl::shared_ptr& channel); + + /// Cancel any open listener operations and clear them out. + void cancelListeners(); + + /// Stop all hearbeats. + void stopHeartbeats(); + + // PRIVATE ACCESSORS + /// @brief Check that the TCP interfaces are valid. /// /// We require the following: @@ -490,11 +517,11 @@ class TCPSessionFactory { /// @returns 0 on success, nonzero on failure. int validateTcpInterfaces() const; - /// Cancel any open listener operations and clear them out. - void cancelListeners(); - - /// Stop all hearbeats - void stopHeartbeats(); + /// Handle an authentication event for the specified `event` by + /// performing reauthentication using the authentication context stored + /// in the specified `channelInfo`. + void reauthnOnAuthenticationEvent(const bmqp::Event& event, + const ChannelInfo* channelInfo) const; private: // NOT IMPLEMENTED @@ -518,9 +545,10 @@ class TCPSessionFactory { TCPSessionFactory(const mqbcfg::TcpInterfaceConfig& config, bdlmt::EventScheduler* scheduler, bdlbb::BlobBufferFactory* blobBufferFactory, - InitialConnectionHandler* initialConnectionHandler, - mqbstat::StatController* statController, - bslma::Allocator* allocator); + Authenticator* authenticator, + Negotiator* negotiator, + mqbstat::StatController* statController, + bslma::Allocator* allocator); /// Destructor virtual ~TCPSessionFactory(); @@ -564,14 +592,14 @@ class TCPSessionFactory { /// provided by a call to the specified `resultCallback`; or return a /// non-zero code on error, in which case `resultCallback` will never be /// invoked. The optionally specified `negotiationUserData` will be - /// passed in to the `negotiate` method of the Negotiator (through the - /// InitialConnectionContext). The optionally specified - /// `resultState` will be used to set the initial value of the - /// corresponding member of the `InitialConnectionContext` that will - /// be created for negotiation of this session; so that it can be retrieved - /// in the `negotiationComplete` callback method. The optionally specified - /// `shouldAutoReconnect` will be used to determine if the factory should - /// attempt to reconnect upon loss of connection. + /// passed in to the `handleInitialConnection` method (through the + /// InitialConnectionContext). The optionally specified `resultState` will + /// be used to set the initial value of the corresponding member of the + /// `InitialConnectionContext` that will be created for negotiation of this + /// session; so that it can be retrieved in the `initialConnectionComplete` + /// callback method. The optionally specified `shouldAutoReconnect` will + /// be used to determine if the factory should attempt to reconnect upon + /// loss of connection. int connect(const bslstl::StringRef& endpoint, const ResultCallback& resultCallback, bslma::ManagedPtr* negotiationUserData = 0, diff --git a/src/groups/mqb/mqbnet/mqbnet_transportmanager.cpp b/src/groups/mqb/mqbnet/mqbnet_transportmanager.cpp index 82dac9da3a..ac1cfd03d1 100644 --- a/src/groups/mqb/mqbnet/mqbnet_transportmanager.cpp +++ b/src/groups/mqb/mqbnet/mqbnet_transportmanager.cpp @@ -115,14 +115,15 @@ int TransportManager::createAndStartTcpInterface( bslma::Allocator* alloc = d_allocators.get("Interface" + bsl::to_string(config.port())); - d_tcpSessionFactory_mp.load( - new (*alloc) TCPSessionFactory(config, - d_scheduler_p, - d_blobBufferFactory_p, - d_initialConnectionHandler_mp.get(), - d_statController_p, - alloc), - alloc); + d_tcpSessionFactory_mp.load(new (*alloc) + TCPSessionFactory(config, + d_scheduler_p, + d_blobBufferFactory_p, + d_authenticator_mp.get(), + d_negotiator_mp.get(), + d_statController_p, + alloc), + alloc); return d_tcpSessionFactory_mp->start(errorDescription); } @@ -336,16 +337,18 @@ int TransportManager::selfNodeIdLocked( } TransportManager::TransportManager( - bdlmt::EventScheduler* scheduler, - bdlbb::BlobBufferFactory* blobBufferFactory, - bslma::ManagedPtr& initialConnectionHandler, - mqbstat::StatController* statController, - bslma::Allocator* allocator) + bdlmt::EventScheduler* scheduler, + bdlbb::BlobBufferFactory* blobBufferFactory, + bslma::ManagedPtr& authenticator, + bslma::ManagedPtr& negotiator, + mqbstat::StatController* statController, + bslma::Allocator* allocator) : d_allocators(allocator) , d_state(e_STOPPED) , d_scheduler_p(scheduler) , d_blobBufferFactory_p(blobBufferFactory) -, d_initialConnectionHandler_mp(initialConnectionHandler) +, d_authenticator_mp(authenticator) +, d_negotiator_mp(negotiator) , d_statController_p(statController) , d_tcpSessionFactory_mp(0) , d_connectionsState(allocator) @@ -371,7 +374,8 @@ int TransportManager::start(bsl::ostream& errorDescription) enum RcEnum { // Value for the various RC error categories rc_SUCCESS = 0, - rc_TCP_INTERFACE = -1 + rc_TCP_INTERFACE = -1, + rc_AUTHENTICATOR = -2 }; BALL_LOG_INFO << "Starting TransportManager"; @@ -392,6 +396,12 @@ int TransportManager::start(bsl::ostream& errorDescription) } } + // Start the Authenticator + rc = d_authenticator_mp->start(errorDescription); + if (rc != 0) { + return (rc * 10) + rc_AUTHENTICATOR; // RETURN + } + d_state = e_STARTING; return rc_SUCCESS; @@ -460,6 +470,11 @@ void TransportManager::stop() d_state = e_STOPPED; + // Stop Authenticator + if (d_authenticator_mp) { + d_authenticator_mp->stop(); + } + // Stop interfaces if (d_tcpSessionFactory_mp) { d_tcpSessionFactory_mp->stop(); diff --git a/src/groups/mqb/mqbnet/mqbnet_transportmanager.h b/src/groups/mqb/mqbnet/mqbnet_transportmanager.h index bc9f807ef6..7288e706f9 100644 --- a/src/groups/mqb/mqbnet/mqbnet_transportmanager.h +++ b/src/groups/mqb/mqbnet/mqbnet_transportmanager.h @@ -61,8 +61,8 @@ // MQB #include +#include #include -#include #include #include @@ -178,8 +178,11 @@ class TransportManager { // BlobBufferFactory to use by the // sessions - // Initial Connection to use - bslma::ManagedPtr d_initialConnectionHandler_mp; + /// Authenticator to use for authenticating a connection. + bslma::ManagedPtr d_authenticator_mp; + + /// Negotiator to use for negotiating and creating a session. + bslma::ManagedPtr d_negotiator_mp; mqbstat::StatController* d_statController_p; // Stat controller @@ -300,12 +303,12 @@ class TransportManager { /// Create a new `TransportManager` using the specified `scheduler`, /// `blobBufferFactory`, `negotiator` and `statController` and the /// specified `allocator` for any memory allocation. - TransportManager( - bdlmt::EventScheduler* scheduler, - bdlbb::BlobBufferFactory* blobBufferFactory, - bslma::ManagedPtr& initialConnectionHandler, - mqbstat::StatController* statController, - bslma::Allocator* allocator); + TransportManager(bdlmt::EventScheduler* scheduler, + bdlbb::BlobBufferFactory* blobBufferFactory, + bslma::ManagedPtr& authenticator, + bslma::ManagedPtr& negotiator, + mqbstat::StatController* statController, + bslma::Allocator* allocator); /// Destructor virtual ~TransportManager(); diff --git a/src/groups/mqb/mqbnet/package/mqbnet.mem b/src/groups/mqb/mqbnet/package/mqbnet.mem index 72b7c2d44b..e9b02b348f 100644 --- a/src/groups/mqb/mqbnet/package/mqbnet.mem +++ b/src/groups/mqb/mqbnet/package/mqbnet.mem @@ -1,3 +1,5 @@ +mqbnet_authenticationcontext +mqbnet_authenticator mqbnet_channel mqbnet_cluster mqbnet_clusteractivenodemanager @@ -6,7 +8,6 @@ mqbnet_connectiontype mqbnet_dummysession mqbnet_elector mqbnet_initialconnectioncontext -mqbnet_initialconnectionhandler mqbnet_mockcluster mqbnet_multirequestmanager mqbnet_negotiationcontext diff --git a/src/integration-tests/test_authn.py b/src/integration-tests/test_authn.py new file mode 100644 index 0000000000..16d01d5374 --- /dev/null +++ b/src/integration-tests/test_authn.py @@ -0,0 +1,674 @@ +# Copyright 2025 Bloomberg Finance L.P. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Authentication test suite using ONLY built-in authenticators. + +This test suite validates authentication logic without any external plugins. +All tests use the built-in authenticators: + - BasicAuthenticator: BASIC mechanism, validates credentials from config + Config format: {"key": "username", "value": {"stringVal": "password"}} + - AnonPassAuthenticator: ANONYMOUS mechanism, always passes (default) + - AnonFailAuthenticator: ANONYMOUS mechanism, always fails + +This approach tests all authentication scenarios without needing external plugins. +""" + +import threading +import pytest + +import blazingmq.dev.it.testconstants as tc +from blazingmq.dev.it.process.rawclient import RawClient +from blazingmq.dev.it.process.admin import AdminClient +from blazingmq.dev.it.process.proc import ProcessExitError + +from blazingmq.dev.it.fixtures import ( # pylint: disable=unused-import + Cluster, + order, + single_node, + tweak, + start_cluster, +) + +pytestmark = order(99) + + +# ============================================================================== +# Basic Authentication Tests +# ============================================================================== + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + } + ] + } +) +def test_authenticate_basic_success(single_node: Cluster) -> None: + """Test successful authentication with built-in BasicAuthenticator.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + auth_resp = client.send_authentication_request("Basic", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + + nego_resp = client.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + client.stop() + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + } + ] + } +) +def test_authenticate_basic_failure(single_node: Cluster) -> None: + """Test failed authentication with invalid credentials.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Invalid credentials should fail + auth_resp = client.send_authentication_request("Basic", "invalid:wrong") + assert auth_resp["authenticationResponse"]["status"]["code"] != 0 + + client.stop() + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user0", "value": {"stringVal": "password0"}}, + {"key": "user1", "value": {"stringVal": "password1"}}, + {"key": "user2", "value": {"stringVal": "password2"}}, + {"key": "user3", "value": {"stringVal": "password3"}}, + {"key": "user4", "value": {"stringVal": "password4"}}, + {"key": "user5", "value": {"stringVal": "password5"}}, + {"key": "user6", "value": {"stringVal": "password6"}}, + {"key": "user7", "value": {"stringVal": "password7"}}, + ], + } + ] + } +) +def test_authenticate_concurrent(single_node: Cluster) -> None: + """Test concurrent authentication with BasicAuthenticator.""" + num_threads = 8 + results = [None] * num_threads + threads = [] + + def auth_worker(idx): + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + auth_resp = client.send_authentication_request( + "Basic", f"user{idx}:password{idx}" + ) + results[idx] = auth_resp["authenticationResponse"]["status"]["code"] + client.send_negotiation_request() + client.stop() + + for i in range(num_threads): + t = threading.Thread(target=auth_worker, args=(i,)) + threads.append(t) + t.start() + + for t in threads: + t.join() + + # Assert all authentications succeeded + assert all(code == 0 for code in results), f"Some authentications failed: {results}" + + +# ============================================================================== +# Reauthentication Tests +# ============================================================================== + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + {"key": "user2", "value": {"stringVal": "password2"}}, + ], + } + ] + } +) +def test_reauthenticate_success(single_node: Cluster) -> None: + """Test successful reauthentication with same credentials.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Initial authentication + auth_resp = client.send_authentication_request("Basic", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + assert auth_resp["authenticationResponse"]["lifetimeMs"] == 600000 + + nego_resp = client.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + # Reauthentication with same credentials + auth_resp = client.send_authentication_request("Basic", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + + # Reauthentication with different credentials + auth_resp = client.send_authentication_request("Basic", "user2:password2") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + + client.stop() + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + } + ] + } +) +def test_reauthenticate_failure(single_node: Cluster) -> None: + """Test reauthentication failure with invalid credentials.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Initial authentication succeeds + auth_resp = client.send_authentication_request("Basic", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + assert auth_resp["authenticationResponse"]["lifetimeMs"] == 600000 + + nego_resp = client.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + # Reauthentication with wrong credentials fails + auth_resp = client.send_authentication_request("Basic", "user1:wrongpass") + assert auth_resp["authenticationResponse"]["status"]["code"] != 0 + + # Connection should be closed after failed reauthentication + with pytest.raises(ConnectionError): + client.send_negotiation_request() + + client.stop() + + +# ============================================================================== +# Default Anonymous Credential Tests +# ============================================================================== + + +def test_default_anonymous_single_node(single_node: Cluster) -> None: + """Test default anonymous authentication on single node.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Should succeed with default AnonPassAuthenticator + nego_resp = client.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + client.stop() + + +def test_default_anonymous_multi_node( + multi_node: Cluster, + sc_domain_urls: tc.DomainUrls, # pylint: disable=unused-argument +) -> None: + """Test default anonymous authentication on multi-node cluster.""" + client = RawClient() + client.open_channel(*multi_node.admin_endpoint) + + nego_resp = client.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + client.stop() + + +# ============================================================================== +# Anonymous Credential Configuration Tests +# ============================================================================== + + +@tweak.broker.app_config.authentication({"anonymousCredential": {"disallow": {}}}) +def test_anonymous_disallowed(single_node: Cluster) -> None: + """Test that anonymous authentication can be disallowed.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Should fail when anonymous is disallowed + with pytest.raises(ConnectionError): + client.send_negotiation_request() + + client.stop() + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + } + ], + "anonymousCredential": { + "credential": {"mechanism": "Basic", "identity": "user1:wrongpass"} + }, + } +) +def test_anonymous_credential_invalid(single_node: Cluster) -> None: + """Test negotiation with invalid anonymous credential.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Should fail with invalid anonymousCredential + with pytest.raises(ConnectionError): + client.send_negotiation_request() + + client.stop() + + +# ============================================================================== +# Empty Authenticators Tests +# ============================================================================== + + +def test_empty_authenticators_reject_basic(single_node: Cluster) -> None: + """Empty authenticators should reject non-ANONYMOUS mechanisms.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Should reject Basic when only default AnonPass is available + auth_resp = client.send_authentication_request("Basic", "user:pass") + assert auth_resp["authenticationResponse"]["status"]["code"] != 0 + + client.stop() + + +# ============================================================================== +# BasicAuthenticator Configuration Tests +# ============================================================================== + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + } + ] + } +) +def test_basic_auth_allows_anonymous(single_node: Cluster) -> None: + """Test that BasicAuthenticator coexists with default anonymous (AnonPass).""" + # Should allow anonymous negotiation (default AnonPass still active) + client1 = RawClient() + client1.open_channel(*single_node.admin_endpoint) + + nego_resp = client1.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + client1.stop() + + # Should also allow Basic authentication + client2 = RawClient() + client2.open_channel(*single_node.admin_endpoint) + + auth_resp = client2.send_authentication_request("Basic", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + + nego_resp = client2.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + client2.stop() + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + } + ] + } +) +def test_basic_auth_rejects_other_mechanisms(single_node: Cluster) -> None: + """BasicAuthenticator should reject unsupported mechanisms.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Should reject unsupported mechanism + auth_resp = client.send_authentication_request("OAuth", "token") + assert auth_resp["authenticationResponse"]["status"]["code"] != 0 + + client.stop() + + +# ============================================================================== +# Anonymous Credential Matching Tests +# ============================================================================== + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + }, + ], + "anonymousCredential": { + "credential": {"mechanism": "Basic", "identity": "user1:password1"} + }, + } +) +def test_anonymous_credential_mechanism_match(single_node: Cluster) -> None: + """Test anonymous credential with matching authenticator.""" + # Test explicit authentication + client1 = RawClient() + client1.open_channel(*single_node.admin_endpoint) + + auth_resp = client1.send_authentication_request("Basic", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + + nego_resp = client1.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + client1.stop() + + # Test default authentication via negotiation + client2 = RawClient() + client2.open_channel(*single_node.admin_endpoint) + + nego_resp = client2.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + + client2.stop() + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + }, + ], + "anonymousCredential": {"disallow": {}}, + } +) +def test_basic_with_anonymous_disallowed(single_node: Cluster) -> None: + """Test BasicAuthenticator with anonymous disallowed.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Should fail negotiation without authentication + with pytest.raises(ConnectionError): + client.send_negotiation_request() + + client.stop() + + +# ============================================================================== +# Edge Cases +# ============================================================================== + + +@tweak.broker.app_config.authentication( + { + "authenticators": [], + "anonymousCredential": {"disallow": {}}, + } +) +def test_no_authentication_possible(single_node: Cluster) -> None: + """Test that no connection is possible when configured that way.""" + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Cannot negotiate - no way to authenticate + with pytest.raises(ConnectionError): + client.send_negotiation_request() + + client.stop() + + +# ============================================================================== +# Case Sensitivity Tests +# ============================================================================== + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + }, + ], + "anonymousCredential": { + "credential": {"mechanism": "BASIC", "identity": "user1:password1"} + }, + } +) +def test_mechanism_case_insensitive(single_node: Cluster) -> None: + """Test that mechanism names are case-insensitive.""" + # Test lowercase + client1 = RawClient() + client1.open_channel(*single_node.admin_endpoint) + auth_resp = client1.send_authentication_request("basic", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + nego_resp = client1.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + client1.stop() + + # Test uppercase + client2 = RawClient() + client2.open_channel(*single_node.admin_endpoint) + auth_resp = client2.send_authentication_request("BASIC", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + nego_resp = client2.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + client2.stop() + + # Test mixed case + client3 = RawClient() + client3.open_channel(*single_node.admin_endpoint) + auth_resp = client3.send_authentication_request("BaSiC", "user1:password1") + assert auth_resp["authenticationResponse"]["status"]["code"] == 0 + nego_resp = client3.send_negotiation_request() + assert nego_resp["brokerResponse"]["result"]["code"] == 0 + client3.stop() + + +# ============================================================================== +# Admin Client Tests +# ============================================================================== + + +def test_admin_with_default_anonymous(single_node: Cluster) -> None: + """Test admin commands with default anonymous authentication.""" + admin = AdminClient() + admin.connect(*single_node.admin_endpoint) + + assert ( + "This process responds to the following CMD subcommands:" + in admin.send_admin("help") + ) + + admin.stop() + + +# ============================================================================== +# AnonFailAuthenticator Tests (if needed for negative testing) +# ============================================================================== + + +@tweak.broker.app_config.authentication( + { + "authenticators": [ + {"name": "AnonFailAuthenticator", "configs": []}, + ], + "anonymousCredential": { + "credential": {"mechanism": "ANONYMOUS", "identity": ""} + }, + } +) +def test_anon_fail_authenticator(single_node: Cluster) -> None: + """ + Test AnonFailAuthenticator that always fails. + This tests the scenario where ANONYMOUS mechanism exists but fails authentication. + """ + client = RawClient() + client.open_channel(*single_node.admin_endpoint) + + # Should fail with AnonFailAuthenticator + with pytest.raises(ConnectionError): + client.send_negotiation_request() + + client.stop() + + +# ============================================================================== +# Broker Startup Failure Tests +# ============================================================================== + + +def check_fail_to_start(node: Cluster): + # Try to start the cluster - should fail due to mismatched mechanism + try: + with pytest.raises(ProcessExitError) as exc_info: + node.start(wait_leader=False) + finally: + for proc in node.all_processes: + proc.check_exit_code = False + + # Verify it's the expected process that failed + assert "single" in str(exc_info.value) + + # Verify broker is not running + node = node.nodes()[0] + assert not node.is_alive(), ( + "Broker should not be running after trying to start with a wrong configuration" + ) + + +@start_cluster(False) +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "AnonPassAuthenticator", + "configs": [], + }, + { + "name": "AnonFailAuthenticator", # Duplicate mechanism ANONYMOUS + "configs": [], + }, + ] + } +) +def test_duplicate_mechanism_fails_startup(single_node: Cluster) -> None: + """ + Test that broker fails at startup when two authenticators have the same mechanism. + + This validates that AuthenticationController::initializeAuthenticators() properly + detects duplicate mechanisms and prevents broker startup. + """ + + check_fail_to_start(single_node) + + +@start_cluster(False) +@tweak.broker.app_config.authentication( + { + "authenticators": [ + { + "name": "BasicAuthenticator", + "configs": [ + {"key": "user1", "value": {"stringVal": "password1"}}, + ], + } + ], + "anonymousCredential": { + "credential": { + "mechanism": "OAuth", # Mechanism not matching any authenticator + "identity": "token123", + } + }, + } +) +def test_mismatched_anonymous_credential_fails_startup(single_node: Cluster) -> None: + """ + Test that broker fails at startup when anonymousCredential uses a mechanism + that doesn't match any configured authenticator. + + This validates the bidirectional validation logic in validateAnonymousCredential(). + """ + check_fail_to_start(single_node) + + +@start_cluster(False) +@tweak.broker.app_config.authentication( + { + "authenticators": [ + {"name": "AnonPassAuthenticator", "configs": []}, + ], + # No anonymousCredential specified - custom ANONYMOUS must have credential + } +) +def test_custom_anonymous_without_credential_fails_startup( + single_node: Cluster, +) -> None: + """ + Test that broker fails at startup when a custom ANONYMOUS authenticator + is configured without an anonymousCredential. + + This validates that validateAnonymousCredential() ensures custom ANONYMOUS + authenticators (non-default) must have an explicit credential configured. + """ + check_fail_to_start(single_node) diff --git a/src/python/blazingmq/dev/configurator/__init__.py b/src/python/blazingmq/dev/configurator/__init__.py index ee35f56e03..1158b43461 100644 --- a/src/python/blazingmq/dev/configurator/__init__.py +++ b/src/python/blazingmq/dev/configurator/__init__.py @@ -422,6 +422,10 @@ class Proto: min_cpp_sdk_version=11207, min_java_sdk_version=10, ), + authentication=mqbcfg.AuthenticatorConfig( + authenticators=[], + anonymous_credential=None, + ), ), ) ) diff --git a/src/python/blazingmq/dev/fuzztest/__init__.py b/src/python/blazingmq/dev/fuzztest/__init__.py index d8999758ec..f8d1d4627a 100644 --- a/src/python/blazingmq/dev/fuzztest/__init__.py +++ b/src/python/blazingmq/dev/fuzztest/__init__.py @@ -359,7 +359,7 @@ def fuzz(host: str, port: int, request: Optional[str] = None) -> None: session = boofuzz.Session( target=boofuzz.Target( - connection=boofuzz.TCPSocketConnection(host, port, recv_timeout=0.05) + connection=boofuzz.TCPSocketConnection(host, port, recv_timeout=0.1) ), receive_data_after_each_request=True, receive_data_after_fuzz=True, diff --git a/src/python/blazingmq/dev/it/process/rawclient.py b/src/python/blazingmq/dev/it/process/rawclient.py index b895eae6ef..00ee1f04fd 100644 --- a/src/python/blazingmq/dev/it/process/rawclient.py +++ b/src/python/blazingmq/dev/it/process/rawclient.py @@ -23,6 +23,7 @@ import socket import json from typing import Optional, Tuple, Union +import base64 from blazingmq.schemas import broker @@ -80,7 +81,35 @@ def _wrap_heartbeat_res_event() -> bytes: return event_size + event_desc - def _send_raw(self, message: bytes) -> None: + @staticmethod + def _wrap_authentication_event(payload: Union[str, dict]) -> bytes: + """ + Wraps the specified 'payload' with EventHeader and adds padding to the + end. Returns the raw bytes authentication message. + + See also: bmqp::EventHeader + """ + + if isinstance(payload, str): + payload_str = payload + else: + payload_str = json.dumps(payload) + + padding_len = 4 - len(payload_str) % 4 + padding = bytes([padding_len] * padding_len) + + event_type = broker.EventType.AUTHENTICATION + type_specific = broker.TypeSpecific.ENCODING_JSON + + auth_header_bytes = 8 + auth_event_size = (auth_header_bytes + len(payload_str) + padding_len).to_bytes( + 4, "big" + ) + auth_event_desc = bytes([0x40 + event_type, 0x02, type_specific, 0x00]) + + return auth_event_size + auth_event_desc + payload_str.encode("ascii") + padding + + def _send_raw(self, message: bytes) -> Tuple[bytes, bytes]: """ Send the specified raw "message" over the channel to the broker. Return the received byte response. @@ -171,7 +200,8 @@ def _receive_event(self) -> Tuple[bytes, bytes]: def open_channel(self, host: str, port: int) -> None: """ Open a new channel to the broker using the specified 'host' / 'port'. - This method is used to establish a connection for sending negotiation requests. + This method is used to establish a connection for sending + authentication and negotiation requests. """ assert self._channel is None @@ -199,6 +229,33 @@ def decode_event_bytes(self, response_header: bytes, response_body: bytes) -> di print(f"Unknown encoding type: {type_specific}") raise ValueError("Unknown encoding in response") + def send_authentication_request( + self, auth_mechanism: str, auth_data: Union[str, bytes] + ) -> dict: + """ + Send an authentication request to the broker with the specified + authentication mechanism 'auth_mechanism' and authentication data 'auth_data'. + """ + assert self._channel is not None + + auth_request = broker.AUTHENTICATE_REQUEST_SCHEMA + + if isinstance(auth_data, str): + raw_bytes = auth_data.encode("utf-8") + else: + raw_bytes = auth_data # already bytes + + auth_request["authenticationRequest"]["mechanism"] = auth_mechanism + auth_request["authenticationRequest"]["data"] = base64.b64encode( + raw_bytes + ).decode("ascii") + + self._send_raw(self._wrap_authentication_event(auth_request)) + response_header, response_body = self._receive_event() + + response = self.decode_event_bytes(response_header, response_body) + return response + def send_negotiation_request(self) -> dict: """ Send a negotiation request to the broker. diff --git a/src/python/blazingmq/dev/it/tweaks/generated.py b/src/python/blazingmq/dev/it/tweaks/generated.py index 97b4269374..e702ebd057 100644 --- a/src/python/blazingmq/dev/it/tweaks/generated.py +++ b/src/python/blazingmq/dev/it/tweaks/generated.py @@ -832,6 +832,16 @@ def __call__( anonymous_credential = AnonymousCredential() + class MinThreads(metaclass=TweakMetaclass): + def __call__(self, value: int) -> Callable: ... + + min_threads = MinThreads() + + class MaxThreads(metaclass=TweakMetaclass): + def __call__(self, value: int) -> Callable: ... + + max_threads = MaxThreads() + def __call__( self, value: typing.Union[ diff --git a/src/python/blazingmq/schemas/mqbcfg.py b/src/python/blazingmq/schemas/mqbcfg.py index d2de58b499..c5edf64b5a 100644 --- a/src/python/blazingmq/schemas/mqbcfg.py +++ b/src/python/blazingmq/schemas/mqbcfg.py @@ -1866,6 +1866,10 @@ class AuthenticatorConfig: uses the provided credential with a matching plugin from `authenticators`. When omitted, the broker defaults to AnonAuthenticator and always passes for anonymous authentication. + minThreads..............: + Minimum number of threads in the authentication thread pool. + maxThreads..............: + Maximum number of threads in the authentication thread pool. """ authenticators: List[AuthenticatorPluginConfig] = field( @@ -1883,6 +1887,24 @@ class AuthenticatorConfig: "namespace": "http://bloomberg.com/schemas/mqbcfg", }, ) + min_threads: int = field( + default=1, + metadata={ + "name": "minThreads", + "type": "Element", + "namespace": "http://bloomberg.com/schemas/mqbcfg", + "required": True, + }, + ) + max_threads: int = field( + default=3, + metadata={ + "name": "maxThreads", + "type": "Element", + "namespace": "http://bloomberg.com/schemas/mqbcfg", + "required": True, + }, + ) @dataclass