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/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)));