diff options
-rw-r--r-- | crow/include/crow/app.h | 2 | ||||
-rw-r--r-- | crow/include/crow/routing.h | 252 | ||||
-rw-r--r-- | include/http_utility.hpp | 2 | ||||
-rw-r--r-- | include/openbmc_dbus_rest.hpp | 29 | ||||
-rw-r--r-- | redfish-core/include/node.hpp | 125 | ||||
-rw-r--r-- | redfish-core/include/privileges.hpp | 4 | ||||
-rw-r--r-- | redfish-core/include/redfish.hpp | 5 | ||||
-rw-r--r-- | redfish-core/lib/account_service.hpp | 1 | ||||
-rw-r--r-- | redfish-core/lib/redfish_sessions.hpp | 1 |
9 files changed, 292 insertions, 129 deletions
diff --git a/crow/include/crow/app.h b/crow/include/crow/app.h index f1f3687723..95bbaed0bc 100644 --- a/crow/include/crow/app.h +++ b/crow/include/crow/app.h @@ -1,5 +1,7 @@ #pragma once +#include "privileges.hpp" + #include <chrono> #include <cstdint> #include <functional> diff --git a/crow/include/crow/routing.h b/crow/include/crow/routing.h index 746e115894..e657e2ef1b 100644 --- a/crow/include/crow/routing.h +++ b/crow/include/crow/routing.h @@ -1,7 +1,9 @@ #pragma once -#include "boost/container/flat_map.hpp" +#include "privileges.hpp" +#include <boost/container/flat_map.hpp> +#include <boost/container/small_vector.hpp> #include <boost/lexical_cast.hpp> #include <cerrno> #include <cstdint> @@ -21,10 +23,14 @@ namespace crow { + +constexpr int maxHttpVerbCount = + static_cast<int>(boost::beast::http::verb::unlink); + class BaseRule { public: - BaseRule(std::string rule) : rule(std::move(rule)) + BaseRule(std::string thisRule) : rule(std::move(thisRule)) { } @@ -62,9 +68,29 @@ class BaseRule return methodsBitfield; } - protected: + bool checkPrivileges(const redfish::Privileges& userPrivileges) + { + // If there are no privileges assigned, assume no privileges + // required + if (privilegesSet.empty()) + { + return true; + } + + for (const redfish::Privileges& requiredPrivileges : privilegesSet) + { + if (userPrivileges.isSupersetOf(requiredPrivileges)) + { + return true; + } + } + return false; + } + uint32_t methodsBitfield{1 << (int)boost::beast::http::verb::get}; + std::vector<redfish::Privileges> privilegesSet; + std::string rule; std::string nameStr; @@ -178,16 +204,11 @@ template <typename Func, typename... ArgsWrapped> struct Wrapped const Request&>::value, int>::type = 0) { - handler = ( -#ifdef BMCWEB_CAN_USE_CPP14 - [f = std::move(f)] -#else - [f] -#endif - (const Request&, Response& res, Args... args) { - res = Response(f(args...)); - res.end(); - }); + handler = [f = std::move(f)](const Request&, Response& res, + Args... args) { + res = Response(f(args...)); + res.end(); + }; } template <typename Req, typename... Args> struct ReqHandlerWrapper @@ -362,7 +383,7 @@ template <typename T> struct RuleParameterTraits using self_t = T; WebSocketRule& websocket() { - auto p = new WebSocketRule(((self_t*)this)->rule); + WebSocketRule* p = new WebSocketRule(((self_t*)this)->rule); ((self_t*)this)->ruleToUpgrade.reset(p); return *p; } @@ -386,6 +407,23 @@ template <typename T> struct RuleParameterTraits ((self_t*)this)->methodsBitfield |= 1 << (int)method; return (self_t&)*this; } + + template <typename... MethodArgs> + self_t& requires(std::initializer_list<const char*> l) + { + ((self_t*)this)->privilegesSet.emplace_back(l); + return (self_t&)*this; + } + + template <typename... MethodArgs> + self_t& requires(const std::vector<redfish::Privileges>& p) + { + for (const redfish::Privileges& privilege : p) + { + ((self_t*)this)->privilegesSet.emplace_back(privilege); + } + return (self_t&)*this; + } }; class DynamicRule : public BaseRule, public RuleParameterTraits<DynamicRule> @@ -547,7 +585,8 @@ class TaggedRule : public BaseRule, std::is_same<void, decltype(f(std::declval<crow::Request>(), std::declval<crow::Response&>(), std::declval<Args>()...))>::value, - "Handler function with response argument should have void return " + "Handler function with response argument should have void " + "return " "type"); handler = std::move(f); @@ -599,7 +638,7 @@ class Trie private: void optimizeNode(Node* node) { - for (auto x : node->paramChildrens) + for (unsigned int x : node->paramChildrens) { if (!x) continue; @@ -609,7 +648,7 @@ class Trie if (node->children.empty()) return; bool mergeWithChild = true; - for (auto& kv : node->children) + for (const std::pair<std::string, unsigned>& kv : node->children) { Node* child = &nodes[kv.second]; if (!child->isSimpleNode()) @@ -621,10 +660,11 @@ class Trie if (mergeWithChild) { decltype(node->children) merged; - for (auto& kv : node->children) + for (const std::pair<std::string, unsigned>& kv : node->children) { Node* child = &nodes[kv.second]; - for (auto& childKv : child->children) + for (const std::pair<std::string, unsigned>& childKv : + child->children) { merged[kv.first + childKv.first] = childKv.second; } @@ -634,7 +674,7 @@ class Trie } else { - for (auto& kv : node->children) + for (const std::pair<std::string, unsigned>& kv : node->children) { Node* child = &nodes[kv.second]; optimizeNode(child); @@ -658,13 +698,13 @@ class Trie void findRouteIndexes(const std::string& req_url, std::vector<unsigned>& route_indexes, - const Node* node = nullptr, unsigned pos = 0) + const Node* node = nullptr, unsigned pos = 0) const { if (node == nullptr) { node = head(); } - for (auto& kv : node->children) + for (const std::pair<std::string, unsigned>& kv : node->children) { const std::string& fragment = kv.first; const Node* child = &nodes[kv.second]; @@ -725,7 +765,7 @@ class Trie if (errno != ERANGE && eptr != req_url.data() + pos) { params->intParams.push_back(value); - auto ret = + std::pair<unsigned, RoutingParams> ret = find(req_url, &nodes[node->paramChildrens[(int)ParamType::INT]], eptr - req_url.data(), params); @@ -747,7 +787,7 @@ class Trie if (errno != ERANGE && eptr != req_url.data() + pos) { params->uintParams.push_back(value); - auto ret = + std::pair<unsigned, RoutingParams> ret = find(req_url, &nodes[node->paramChildrens[(int)ParamType::UINT]], eptr - req_url.data(), params); @@ -768,7 +808,7 @@ class Trie if (errno != ERANGE && eptr != req_url.data() + pos) { params->doubleParams.push_back(value); - auto ret = find( + std::pair<unsigned, RoutingParams> ret = find( req_url, &nodes[node->paramChildrens[(int)ParamType::DOUBLE]], eptr - req_url.data(), params); @@ -791,7 +831,7 @@ class Trie { params->stringParams.emplace_back( req_url.substr(pos, epos - pos)); - auto ret = + std::pair<unsigned, RoutingParams> ret = find(req_url, &nodes[node->paramChildrens[(int)ParamType::STRING]], epos, params); @@ -808,7 +848,7 @@ class Trie { params->stringParams.emplace_back( req_url.substr(pos, epos - pos)); - auto ret = find( + std::pair<unsigned, RoutingParams> ret = find( req_url, &nodes[node->paramChildrens[(int)ParamType::PATH]], epos, params); updateFound(ret); @@ -816,14 +856,15 @@ class Trie } } - for (auto& kv : node->children) + for (const std::pair<std::string, unsigned>& kv : node->children) { const std::string& fragment = kv.first; const Node* child = &nodes[kv.second]; if (req_url.compare(pos, fragment.size(), fragment) == 0) { - auto ret = find(req_url, child, pos + fragment.size(), params); + std::pair<unsigned, RoutingParams> ret = + find(req_url, child, pos + fragment.size(), params); updateFound(ret); } } @@ -840,31 +881,29 @@ class Trie char c = url[i]; if (c == '<') { - static struct ParamTraits + const static std::array<std::pair<ParamType, std::string>, 7> + paramTraits = {{ + {ParamType::INT, "<int>"}, + {ParamType::UINT, "<uint>"}, + {ParamType::DOUBLE, "<float>"}, + {ParamType::DOUBLE, "<double>"}, + {ParamType::STRING, "<str>"}, + {ParamType::STRING, "<string>"}, + {ParamType::PATH, "<path>"}, + }}; + + for (const std::pair<ParamType, std::string>& x : paramTraits) { - ParamType type; - std::string name; - } paramTraits[] = { - {ParamType::INT, "<int>"}, - {ParamType::UINT, "<uint>"}, - {ParamType::DOUBLE, "<float>"}, - {ParamType::DOUBLE, "<double>"}, - {ParamType::STRING, "<str>"}, - {ParamType::STRING, "<string>"}, - {ParamType::PATH, "<path>"}, - }; - - for (auto& x : paramTraits) - { - if (url.compare(i, x.name.size(), x.name) == 0) + if (url.compare(i, x.second.size(), x.second) == 0) { - if (!nodes[idx].paramChildrens[(int)x.type]) + if (!nodes[idx].paramChildrens[(int)x.first]) { - auto newNodeIdx = newNode(); - nodes[idx].paramChildrens[(int)x.type] = newNodeIdx; + unsigned newNodeIdx = newNode(); + nodes[idx].paramChildrens[(int)x.first] = + newNodeIdx; } - idx = nodes[idx].paramChildrens[(int)x.type]; - i += x.name.size(); + idx = nodes[idx].paramChildrens[(int)x.first]; + i += x.second.size(); break; } } @@ -876,7 +915,7 @@ class Trie std::string piece(&c, 1); if (!nodes[idx].children.count(piece)) { - auto newNodeIdx = newNode(); + unsigned newNodeIdx = newNode(); nodes[idx].children.emplace(piece, newNodeIdx); } idx = nodes[idx].children[piece]; @@ -921,7 +960,7 @@ class Trie debugNodePrint(&nodes[n->paramChildrens[i]], level + 1); } } - for (auto& kv : n->children) + for (const std::pair<std::string, unsigned>& kv : n->children) { BMCWEB_LOG_DEBUG << std::string(2 * level, ' ') /*<< "(" << kv.second << ") "*/ @@ -959,7 +998,7 @@ class Trie class Router { public: - Router() : rules(2) + Router() { } @@ -968,7 +1007,7 @@ class Router std::unique_ptr<DynamicRule> ruleObject = std::make_unique<DynamicRule>(rule); DynamicRule* ptr = ruleObject.get(); - internalAddRuleObject(rule, std::move(ruleObject)); + allRules.emplace_back(std::move(ruleObject)); return *ptr; } @@ -981,45 +1020,67 @@ class Router TaggedRule>; std::unique_ptr<RuleT> ruleObject = std::make_unique<RuleT>(rule); RuleT* ptr = ruleObject.get(); - - internalAddRuleObject(rule, std::move(ruleObject)); + allRules.emplace_back(std::move(ruleObject)); return *ptr; } - void internalAddRuleObject(const std::string& rule, - std::unique_ptr<BaseRule> ruleObject) + void internalAddRuleObject(const std::string& rule, BaseRule* ruleObject) { - rules.emplace_back(std::move(ruleObject)); - trie.add(rule, rules.size() - 1); - - // directory case: - // request to `/about' url matches `/about/' rule - if (rule.size() > 2 && rule.back() == '/') + if (ruleObject == nullptr) + { + return; + } + for (uint32_t method = 0, method_bit = 1; method < maxHttpVerbCount; + method++, method_bit <<= 1) { - trie.add(rule.substr(0, rule.size() - 1), rules.size() - 1); + if (ruleObject->methodsBitfield & method_bit) + { + perMethods[method].rules.emplace_back(ruleObject); + perMethods[method].trie.add( + rule, perMethods[method].rules.size() - 1); + // directory case: + // request to `/about' url matches `/about/' rule + if (rule.size() > 2 && rule.back() == '/') + { + perMethods[method].trie.add( + rule.substr(0, rule.size() - 1), + perMethods[method].rules.size() - 1); + } + } } } void validate() { - trie.validate(); - for (auto& rule : rules) + for (std::unique_ptr<BaseRule>& rule : allRules) { if (rule) { - auto upgraded = rule->upgrade(); + std::unique_ptr<BaseRule> upgraded = rule->upgrade(); if (upgraded) rule = std::move(upgraded); rule->validate(); + internalAddRuleObject(rule->rule, rule.get()); } } + for (PerMethod& perMethod : perMethods) + { + perMethod.trie.validate(); + } } template <typename Adaptor> void handleUpgrade(const Request& req, Response& res, Adaptor&& adaptor) { - auto found = trie.find(req.url); + if (static_cast<int>(req.method()) >= perMethods.size()) + return; + + PerMethod& perMethod = perMethods[(int)req.method()]; + Trie& trie = perMethod.trie; + std::vector<BaseRule*>& rules = perMethod.rules; + + const std::pair<unsigned, RoutingParams>& found = trie.find(req.url); unsigned ruleIndex = found.first; if (!ruleIndex) { @@ -1097,7 +1158,13 @@ class Router void handle(const Request& req, Response& res) { - auto found = trie.find(req.url); + if ((int)req.method() >= perMethods.size()) + return; + PerMethod& perMethod = perMethods[(int)req.method()]; + Trie& trie = perMethod.trie; + std::vector<BaseRule*>& rules = perMethod.rules; + + const std::pair<unsigned, RoutingParams>& found = trie.find(req.url); unsigned ruleIndex = found.first; @@ -1150,6 +1217,19 @@ class Router << (uint32_t)req.method() << " / " << rules[ruleIndex]->getMethods(); + // TODO: load user privileges from configuration as soon as its + // available now we are granting all privileges to everyone. + redfish::Privileges userPrivileges{"Login", "ConfigureManager", + "ConfigureSelf", "ConfigureUsers", + "ConfigureComponents"}; + + if (!rules[ruleIndex]->checkPrivileges(userPrivileges)) + { + res.result(boost::beast::http::status::method_not_allowed); + res.end(); + return; + } + // any uncaught exceptions become 500s try { @@ -1175,23 +1255,41 @@ class Router void debugPrint() { - trie.debugPrint(); + for (int i = 0; i < perMethods.size(); i++) + { + BMCWEB_LOG_DEBUG << methodName((boost::beast::http::verb)i); + perMethods[i].trie.debugPrint(); + } } std::vector<const std::string*> getRoutes(const std::string& parent) { - std::vector<unsigned> x; std::vector<const std::string*> ret; - trie.findRouteIndexes(parent, x); - for (unsigned index : x) + + for (const PerMethod& pm : perMethods) { - ret.push_back(&rules[index]->rule); + std::vector<unsigned> x; + pm.trie.findRouteIndexes(parent, x); + for (unsigned index : x) + { + ret.push_back(&pm.rules[index]->rule); + } } return ret; } private: - std::vector<std::unique_ptr<BaseRule>> rules; - Trie trie; + struct PerMethod + { + std::vector<BaseRule*> rules; + Trie trie; + // rule index 0, 1 has special meaning; preallocate it to avoid + // duplication. + PerMethod() : rules(2) + { + } + }; + std::array<PerMethod, maxHttpVerbCount> perMethods; + std::vector<std::unique_ptr<BaseRule>> allRules; }; } // namespace crow diff --git a/include/http_utility.hpp b/include/http_utility.hpp index e2b1a11f64..b20952b438 100644 --- a/include/http_utility.hpp +++ b/include/http_utility.hpp @@ -1,6 +1,8 @@ #pragma once #include <boost/algorithm/string.hpp> +#include "crow/http_request.h" + namespace http_helpers { inline bool requestPrefersHtml(const crow::Request& req) diff --git a/include/openbmc_dbus_rest.hpp b/include/openbmc_dbus_rest.hpp index ab35bb2efc..9f7a85508b 100644 --- a/include/openbmc_dbus_rest.hpp +++ b/include/openbmc_dbus_rest.hpp @@ -1982,6 +1982,7 @@ inline void handleDBusUrl(const crow::Request &req, crow::Response &res, template <typename... Middlewares> void requestRoutes(Crow<Middlewares...> &app) { BMCWEB_ROUTE(app, "/bus/") + .requires({"Login"}) .methods("GET"_method)( [](const crow::Request &req, crow::Response &res) { res.jsonValue = {{"busses", {{{"name", "system"}}}}, @@ -1990,6 +1991,7 @@ template <typename... Middlewares> void requestRoutes(Crow<Middlewares...> &app) }); BMCWEB_ROUTE(app, "/bus/system/") + .requires({"Login"}) .methods("GET"_method)( [](const crow::Request &req, crow::Response &res) { auto myCallback = [&res](const boost::system::error_code ec, @@ -2018,13 +2020,23 @@ template <typename... Middlewares> void requestRoutes(Crow<Middlewares...> &app) }); BMCWEB_ROUTE(app, "/list/") + .requires({"Login"}) .methods("GET"_method)( [](const crow::Request &req, crow::Response &res) { handleList(res, "/"); }); BMCWEB_ROUTE(app, "/xyz/<path>") - .methods("GET"_method, "PUT"_method, "POST"_method, "DELETE"_method)( + .requires({"Login"}) + .methods("GET"_method)([](const crow::Request &req, crow::Response &res, + const std::string &path) { + std::string objectPath = "/xyz/" + path; + handleDBusUrl(req, res, objectPath); + }); + + BMCWEB_ROUTE(app, "/xyz/<path>") + .requires({"ConfigureComponents", "ConfigureManager"}) + .methods("PUT"_method, "POST"_method, "DELETE"_method)( [](const crow::Request &req, crow::Response &res, const std::string &path) { std::string objectPath = "/xyz/" + path; @@ -2032,14 +2044,24 @@ template <typename... Middlewares> void requestRoutes(Crow<Middlewares...> &app) }); BMCWEB_ROUTE(app, "/org/<path>") - .methods("GET"_method, "PUT"_method, "POST"_method, "DELETE"_method)( + .requires({"Login"}) + .methods("GET"_method)([](const crow::Request &req, crow::Response &res, + const std::string &path) { + std::string objectPath = "/xyz/" + path; + handleDBusUrl(req, res, objectPath); + }); + + BMCWEB_ROUTE(app, "/org/<path>") + .requires({"ConfigureComponents", "ConfigureManager"}) + .methods("PUT"_method, "POST"_method, "DELETE"_method)( [](const crow::Request &req, crow::Response &res, const std::string &path) { - std::string objectPath = "/org/" + path; + std::string objectPath = "/xyz/" + path; handleDBusUrl(req, res, objectPath); }); BMCWEB_ROUTE(app, "/download/dump/<str>/") + .requires({"ConfigureManager"}) .methods("GET"_method)([](const crow::Request &req, crow::Response &res, const std::string &dumpId) { std::regex validFilename("^[\\w\\- ]+(\\.?[\\w\\- ]*)$"); @@ -2083,6 +2105,7 @@ template <typename... Middlewares> void requestRoutes(Crow<Middlewares...> &app) }); BMCWEB_ROUTE(app, "/bus/system/<str>/") + .requires({"Login"}) .methods("GET"_method)([](const crow::Request &req, crow::Response &res, const std::string &Connection) { introspectObjects(Connection, "/", diff --git a/redfish-core/include/node.hpp b/redfish-core/include/node.hpp index 8e94a6f011..58195279e1 100644 --- a/redfish-core/include/node.hpp +++ b/redfish-core/include/node.hpp @@ -57,20 +57,88 @@ class Node template <typename... Params> Node(CrowApp& app, std::string&& entityUrl, Params... params) { - app.routeDynamic(entityUrl.c_str()) - .methods("GET"_method, "PATCH"_method, "POST"_method, - "DELETE"_method)([&](const crow::Request& req, - crow::Response& res, - Params... params) { - std::vector<std::string> paramVec = {params...}; - dispatchRequest(app, req, res, paramVec); - }); + crow::DynamicRule& get = app.routeDynamic(entityUrl.c_str()); + getRule = &get; + get.methods("GET"_method)([this](const crow::Request& req, + crow::Response& res, + Params... params) { + std::vector<std::string> paramVec = {params...}; + doGet(res, req, paramVec); + }); + + crow::DynamicRule& patch = app.routeDynamic(entityUrl.c_str()); + patchRule = &patch; + patch.methods("PATCH"_method)([this](const crow::Request& req, + crow::Response& res, + Params... params) { + std::vector<std::string> paramVec = {params...}; + doPatch(res, req, paramVec); + }); + + crow::DynamicRule& post = app.routeDynamic(entityUrl.c_str()); + postRule = &post; + post.methods("POST"_method)([this](const crow::Request& req, + crow::Response& res, + Params... params) { + std::vector<std::string> paramVec = {params...}; + doPost(res, req, paramVec); + }); + + crow::DynamicRule& delete_ = app.routeDynamic(entityUrl.c_str()); + deleteRule = &delete_; + delete_.methods("DELETE"_method)([this](const crow::Request& req, + crow::Response& res, + Params... params) { + std::vector<std::string> paramVec = {params...}; + doDelete(res, req, paramVec); + }); + } + + void initPrivileges() + { + auto it = entityPrivileges.find(boost::beast::http::verb::get); + if (it != entityPrivileges.end()) + { + if (getRule != nullptr) + { + getRule->requires(it->second); + } + } + it = entityPrivileges.find(boost::beast::http::verb::post); + if (it != entityPrivileges.end()) + { + if (postRule != nullptr) + { + postRule->requires(it->second); + } + } + it = entityPrivileges.find(boost::beast::http::verb::patch); + if (it != entityPrivileges.end()) + { + if (patchRule != nullptr) + { + patchRule->requires(it->second); + } + } + it = entityPrivileges.find(boost::beast::http::verb::delete_); + if (it != entityPrivileges.end()) + { + if (deleteRule != nullptr) + { + deleteRule->requires(it->second); + } + } } virtual ~Node() = default; OperationMap entityPrivileges; + crow::DynamicRule* getRule = nullptr; + crow::DynamicRule* postRule = nullptr; + crow::DynamicRule* patchRule = nullptr; + crow::DynamicRule* deleteRule = nullptr; + protected: // Node is designed to be an abstract class, so doGet is pure virtual virtual void doGet(crow::Response& res, const crow::Request& req, @@ -100,47 +168,6 @@ class Node res.result(boost::beast::http::status::method_not_allowed); res.end(); } - - private: - void dispatchRequest(CrowApp& app, const crow::Request& req, - crow::Response& res, - const std::vector<std::string>& params) - { - auto ctx = - app.template getContext<crow::token_authorization::Middleware>(req); - - if (!isMethodAllowedForUser(req.method(), entityPrivileges, - ctx.session->username)) - { - res.result(boost::beast::http::status::method_not_allowed); - res.end(); - return; - } - - switch (req.method()) - { - case "GET"_method: - doGet(res, req, params); - break; - - case "PATCH"_method: - doPatch(res, req, params); - break; - - case "POST"_method: - doPost(res, req, params); - break; - - case "DELETE"_method: - doDelete(res, req, params); - break; - - default: - res.result(boost::beast::http::status::not_found); - res.end(); - } - return; - } }; } // namespace redfish diff --git a/redfish-core/include/privileges.hpp b/redfish-core/include/privileges.hpp index 3b20c9fda8..ca44551835 100644 --- a/redfish-core/include/privileges.hpp +++ b/redfish-core/include/privileges.hpp @@ -15,7 +15,11 @@ */ #pragma once +#include <crow/logging.h> + +#include <array> #include <bitset> +#include <boost/beast/http/verb.hpp> #include <boost/container/flat_map.hpp> #include <cstdint> #include <vector> diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp index 2a61c52b48..36b50e87c7 100644 --- a/redfish-core/include/redfish.hpp +++ b/redfish-core/include/redfish.hpp @@ -103,6 +103,11 @@ class RedfishService nodes.emplace_back(std::make_unique<SystemsCollection>(app)); nodes.emplace_back(std::make_unique<Systems>(app)); nodes.emplace_back(std::make_unique<SystemActionsReset>(app)); + + for (const auto& node : nodes) + { + node->initPrivileges(); + } } private: diff --git a/redfish-core/lib/account_service.hpp b/redfish-core/lib/account_service.hpp index 9365ebb06f..a5c501d1b6 100644 --- a/redfish-core/lib/account_service.hpp +++ b/redfish-core/lib/account_service.hpp @@ -224,6 +224,7 @@ class AccountService : public Node } } }; + class AccountsCollection : public Node { public: diff --git a/redfish-core/lib/redfish_sessions.hpp b/redfish-core/lib/redfish_sessions.hpp index 59805a0c46..d4085af9a7 100644 --- a/redfish-core/lib/redfish_sessions.hpp +++ b/redfish-core/lib/redfish_sessions.hpp @@ -139,6 +139,7 @@ class SessionCollection : public Node res.jsonValue["Members"].push_back( {{"@odata.id", "/redfish/v1/SessionService/Sessions/" + *uid}}); } + res.jsonValue["Members@odata.count"] = sessionIds.size(); res.jsonValue["@odata.type"] = "#SessionCollection.SessionCollection"; res.jsonValue["@odata.id"] = "/redfish/v1/SessionService/Sessions/"; res.jsonValue["@odata.context"] = |