Aggregation: Add basic authentication support
Add support for basic authentication when connecting to aggregation
sources. This allows satellite BMCs to be authenticated using
username and password credentials.
The implementation:
- Stores credentials alongside URLs in AggregationSource struct
- Validates credentials: no colons, max 40 chars, not empty strings
- Creates Basic Auth headers using base64 encoding
- Only sends Authorization header when both username and password exist
- Adds PATCH handler for updating credentials independently
- Prevents duplicate aggregation sources with same hostname
- Cleans up credentials when aggregation sources are deleted
Tested: Manual testing with authenticated aggregation sources
Change-Id: Ide17a3c08a4a8f6b90a2ffcd2c798cbbec578db8
Signed-off-by: Kamran Hasan <khasan@nvidia.com>
diff --git a/http/utility.hpp b/http/utility.hpp
index e19e258..bf65b25 100644
--- a/http/utility.hpp
+++ b/http/utility.hpp
@@ -232,6 +232,18 @@
return out;
}
+inline std::string createBasicAuthHeader(std::string_view username,
+ std::string_view password)
+{
+ std::string credentials = "Basic ";
+ Base64Encoder enc;
+ enc.encode(username, credentials);
+ enc.encode(":", credentials);
+ enc.encode(password, credentials);
+ enc.finalize(credentials);
+ return credentials;
+}
+
template <bool urlsafe = false>
inline bool base64Decode(std::string_view input, std::string& output)
{
diff --git a/redfish-core/include/redfish_aggregator.hpp b/redfish-core/include/redfish_aggregator.hpp
index a318e4e..3ac4ed7 100644
--- a/redfish-core/include/redfish_aggregator.hpp
+++ b/redfish-core/include/redfish_aggregator.hpp
@@ -413,6 +413,13 @@
.invalidResp = aggregationRetryHandler};
}
+struct AggregationSource
+{
+ boost::urls::url url;
+ std::string username;
+ std::string password;
+};
+
class RedfishAggregator
{
private:
@@ -814,9 +821,14 @@
{
url.set_query(targetURI.query());
}
+
+ // Prepare request headers
+ boost::beast::http::fields requestFields =
+ prepareAggregationHeaders(thisReq.fields(), prefix);
+
client.sendDataWithCallback(std::move(data), url,
ensuressl::VerifyCertificate::Verify,
- thisReq.fields(), thisReq.method(), cb);
+ requestFields, thisReq.method(), cb);
}
// Forward a request for a collection URI to each known satellite BMC
@@ -837,9 +849,14 @@
url.set_query(thisReq.url().query());
}
std::string data = thisReq.body();
+
+ // Prepare request headers
+ boost::beast::http::fields requestFields =
+ prepareAggregationHeaders(thisReq.fields(), sat.first);
+
client.sendDataWithCallback(std::move(data), url,
ensuressl::VerifyCertificate::Verify,
- thisReq.fields(), thisReq.method(), cb);
+ requestFields, thisReq.method(), cb);
}
}
@@ -864,9 +881,13 @@
std::string data = thisReq.body();
+ // Prepare request headers
+ boost::beast::http::fields requestFields =
+ prepareAggregationHeaders(thisReq.fields(), sat.first);
+
client.sendDataWithCallback(std::move(data), url,
ensuressl::VerifyCertificate::Verify,
- thisReq.fields(), thisReq.method(), cb);
+ requestFields, thisReq.method(), cb);
}
}
@@ -889,8 +910,35 @@
return handler;
}
- // Aggregation sources from AggregationCollection
- std::unordered_map<std::string, boost::urls::url> currentAggregationSources;
+ // Aggregation sources with their URLs and optional credentials
+ std::unordered_map<std::string, AggregationSource> aggregationSources;
+
+ // Helper function to prepare headers for aggregated satellite BMC requests
+ boost::beast::http::fields prepareAggregationHeaders(
+ const boost::beast::http::fields& originalFields,
+ const std::string& prefix) const
+ {
+ boost::beast::http::fields fields = originalFields;
+
+ // POST AggregationService can only parse JSON
+ fields.set(boost::beast::http::field::accept, "application/json");
+
+ // Add authentication if credentials exist for this prefix
+ auto it = aggregationSources.find(prefix);
+ if (it != aggregationSources.end())
+ {
+ const auto& source = it->second;
+ // Only add auth header if both username and password are provided
+ if (!source.username.empty() && !source.password.empty())
+ {
+ std::string authHeader = crow::utility::createBasicAuthHeader(
+ source.username, source.password);
+ fields.set(boost::beast::http::field::authorization,
+ authHeader);
+ }
+ }
+ return fields;
+ }
// Polls D-Bus to get all available satellite config information
// Expects a handler which interacts with the returned configs
@@ -902,8 +950,12 @@
{
BMCWEB_LOG_DEBUG("Gathering satellite configs");
- std::unordered_map<std::string, boost::urls::url> satelliteInfo(
- currentAggregationSources);
+ // Extract just the URLs from aggregationSources for the handler
+ std::unordered_map<std::string, boost::urls::url> satelliteInfo;
+ for (const auto& [prefix, source] : aggregationSources)
+ {
+ satelliteInfo.emplace(prefix, source.url);
+ }
sdbusplus::message::object_path path("/xyz/openbmc_project/inventory");
dbus::utility::getManagedObjects(
@@ -1370,7 +1422,7 @@
bool segmentHasPrefix(const std::string& urlSegment) const
{
// TODO: handle this better
- // For now 5B247A_ wont be in the currentAggregationSources map so
+ // For now 5B247A_ wont be in the aggregationSources map so
// check explicitly for now
if (urlSegment.starts_with("5B247A_"))
{
@@ -1388,7 +1440,7 @@
std::string prefix = urlSegment.substr(0, underscorePos);
// Check if this prefix exists
- return currentAggregationSources.contains(prefix);
+ return aggregationSources.contains(prefix);
}
};
diff --git a/redfish-core/lib/aggregation_service.hpp b/redfish-core/lib/aggregation_service.hpp
index 14c545e..65d3ac7 100644
--- a/redfish-core/lib/aggregation_service.hpp
+++ b/redfish-core/lib/aggregation_service.hpp
@@ -191,6 +191,19 @@
std::string hostName(sat->second.encoded_origin());
asyncResp->res.jsonValue["HostName"] = std::move(hostName);
+ // Include UserName property, defaulting to null
+ auto& aggregator = RedfishAggregator::getInstance();
+ auto it = aggregator.aggregationSources.find(aggregationSourceId);
+ if (it != aggregator.aggregationSources.end() &&
+ !it->second.username.empty())
+ {
+ asyncResp->res.jsonValue["UserName"] = it->second.username;
+ }
+ else
+ {
+ asyncResp->res.jsonValue["UserName"] = nullptr;
+ }
+
// The Redfish spec requires Password to be null in responses
asyncResp->res.jsonValue["Password"] = nullptr;
}
@@ -229,6 +242,36 @@
aggregationSourceId);
}
+inline bool validateCredentialField(const std::optional<std::string>& field,
+ const std::string& fieldName,
+ crow::Response& res)
+{
+ if (!field.has_value())
+ {
+ return true; // Field not provided, that's okay
+ }
+
+ if (field->empty())
+ {
+ messages::stringValueTooShort(res, fieldName, "1");
+ return false;
+ }
+
+ if (field->find(':') != std::string::npos)
+ {
+ messages::propertyValueIncorrect(res, *field, fieldName);
+ return false;
+ }
+
+ if (field->length() > 40)
+ {
+ messages::stringValueTooLong(res, fieldName, 40);
+ return false;
+ }
+
+ return true;
+}
+
inline void handleAggregationSourceCollectionPost(
App& app, const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
@@ -238,10 +281,15 @@
return;
}
std::string hostname;
- if (!json_util::readJsonPatch(req, asyncResp->res, "HostName", hostname))
+ std::optional<std::string> username;
+ std::optional<std::string> password;
+
+ if (!json_util::readJsonPatch(req, asyncResp->res, "HostName", hostname,
+ "UserName", username, "Password", password))
{
return;
}
+
boost::system::result<boost::urls::url> url =
boost::urls::parse_absolute_uri(hostname);
if (!url)
@@ -256,9 +304,35 @@
return;
}
crow::utility::setPortDefaults(*url);
+
+ // Check for duplicate hostname
+ auto& aggregator = RedfishAggregator::getInstance();
+ for (const auto& [existingPrefix, existingSource] :
+ aggregator.aggregationSources)
+ {
+ if (existingSource.url == *url)
+ {
+ messages::resourceAlreadyExists(asyncResp->res, "AggregationSource",
+ "HostName", url->buffer());
+ return;
+ }
+ }
+
+ // Validate username and password
+ if (!validateCredentialField(username, "UserName", asyncResp->res))
+ {
+ return;
+ }
+ if (!validateCredentialField(password, "Password", asyncResp->res))
+ {
+ return;
+ }
+
std::string prefix = bmcweb::getRandomIdOfLength(8);
- RedfishAggregator::getInstance().currentAggregationSources.emplace(
- prefix, *url);
+ aggregator.aggregationSources.emplace(
+ prefix,
+ AggregationSource{*url, username.value_or(""), password.value_or("")});
+
BMCWEB_LOG_DEBUG("Emplaced {} with url {}", prefix, url->buffer());
asyncResp->res.addHeader(
boost::beast::http::field::location,
@@ -267,6 +341,82 @@
messages::created(asyncResp->res);
}
+inline void handleAggregationSourcePatch(
+ App& app, const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& aggregationSourceId)
+{
+ if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+ {
+ return;
+ }
+
+ std::optional<std::string> username;
+ std::optional<std::string> password;
+
+ if (!json_util::readJsonPatch(req, asyncResp->res, "UserName", username,
+ "Password", password))
+ {
+ return;
+ }
+
+ // Validate username and password
+ if (!validateCredentialField(username, "UserName", asyncResp->res))
+ {
+ return;
+ }
+ if (!validateCredentialField(password, "Password", asyncResp->res))
+ {
+ return;
+ }
+
+ // Check if the aggregation source exists in writable sources
+ auto& aggregator = RedfishAggregator::getInstance();
+ auto it = aggregator.aggregationSources.find(aggregationSourceId);
+ if (it != aggregator.aggregationSources.end())
+ {
+ // Update only the fields that were provided
+ if (username.has_value())
+ {
+ it->second.username = *username;
+ }
+ if (password.has_value())
+ {
+ it->second.password = *password;
+ }
+
+ messages::success(asyncResp->res);
+ return;
+ }
+
+ // Not in writable sources, query D-Bus to check if it exists in
+ // Entity Manager sources
+ RedfishAggregator::getInstance().getSatelliteConfigs(
+ [asyncResp, aggregationSourceId](
+ const boost::system::error_code& ec,
+ const std::unordered_map<std::string, boost::urls::url>&
+ satelliteInfo) {
+ // Something went wrong while querying dbus
+ if (ec)
+ {
+ messages::internalError(asyncResp->res);
+ return;
+ }
+
+ // Check if it exists in Entity Manager sources
+ if (satelliteInfo.contains(aggregationSourceId))
+ {
+ // Source exists but is read-only (from Entity Manager)
+ messages::propertyNotWritable(asyncResp->res, "UserName");
+ return;
+ }
+
+ // Doesn't exist anywhere
+ messages::resourceNotFound(asyncResp->res, "AggregationSource",
+ aggregationSourceId);
+ });
+}
+
inline void handleAggregationSourceDelete(
App& app, const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
@@ -280,9 +430,8 @@
boost::beast::http::field::link,
"</redfish/v1/JsonSchemas/AggregationService/AggregationSource.json>; rel=describedby");
- size_t deleted =
- RedfishAggregator::getInstance().currentAggregationSources.erase(
- aggregationSourceId);
+ size_t deleted = RedfishAggregator::getInstance().aggregationSources.erase(
+ aggregationSourceId);
if (deleted == 0)
{
messages::resourceNotFound(asyncResp->res, "AggregationSource",
@@ -303,6 +452,12 @@
BMCWEB_ROUTE(app,
"/redfish/v1/AggregationService/AggregationSources/<str>/")
+ .privileges(redfish::privileges::patchAggregationSource)
+ .methods(boost::beast::http::verb::patch)(
+ std::bind_front(handleAggregationSourcePatch, std::ref(app)));
+
+ BMCWEB_ROUTE(app,
+ "/redfish/v1/AggregationService/AggregationSources/<str>/")
.privileges(redfish::privileges::deleteAggregationSource)
.methods(boost::beast::http::verb::delete_)(
std::bind_front(handleAggregationSourceDelete, std::ref(app)));