| #include "routing.hpp" | |
| #include <array> | |
| #include <cerrno> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <cstdlib> | |
| #include <memory> | |
| #include <optional> | |
| #include <stdexcept> | |
| #include <string> | |
| #include <string_view> | |
| #include <tuple> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/log/log.h" // NOLINT(misc-include-cleaner) | |
| #include "boost/beast/ssl/ssl_stream.hpp" // NOLINT | |
| #include "bmcweb_config.h" // NOLINT(misc-include-cleaner) | |
| #include "common.hpp" | |
| #include "http_request.hpp" | |
| #include "http_response.hpp" | |
| #include "logging.hpp" | |
| #include "utility.hpp" | |
| #include "verb.hpp" | |
| #include "async_resp.hpp" | |
| #include "dbus_utility.hpp" | |
| #include "error_messages.hpp" | |
| #include "privileges.hpp" | |
| #include "dbus_utils.hpp" | |
| #include "grpcpp/support/status.h" // NOLINT(misc-include-cleaner) | |
| #include "bmcweb_authorizer_singleton.h" // NOLINT(misc-include-cleaner) | |
| #include "sdbusplus/unpack_properties.hpp" | |
| namespace crow { | |
| bool Router::isUserPrivileged( | |
| Request& req, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| BaseRule& rule, const dbus::utility::DBusPropertiesMap& userInfoMap) { | |
| std::string user_role{}; | |
| const std::string* user_role_ptr = nullptr; | |
| const bool* remote_user = nullptr; | |
| const bool* password_expired = nullptr; | |
| const bool success = sdbusplus::unpackPropertiesNoThrow( | |
| redfish::dbus_utils::UnpackErrorPrinter(), userInfoMap, "UserPrivilege", | |
| user_role_ptr, "RemoteUser", remote_user, "UserPasswordExpired", | |
| password_expired); | |
| if (!success) { | |
| asyncResp->res.result(boost::beast::http::status::internal_server_error); | |
| return false; | |
| } | |
| if (user_role_ptr != nullptr) { | |
| user_role = *user_role_ptr; | |
| BMCWEB_LOG_DEBUG << "userName = " << req.session->username | |
| << " user_role = " << *user_role_ptr; | |
| } | |
| if (remote_user == nullptr) { | |
| BMCWEB_LOG_ERROR << "RemoteUser property missing or wrong type"; | |
| asyncResp->res.result(boost::beast::http::status::internal_server_error); | |
| return false; | |
| } | |
| bool expired = false; | |
| if (password_expired == nullptr) { | |
| if (!*remote_user) { | |
| BMCWEB_LOG_ERROR << "UserPasswordExpired property is expected for" | |
| " local user but is missing or wrong type"; | |
| asyncResp->res.result(boost::beast::http::status::internal_server_error); | |
| return false; | |
| } | |
| } else { | |
| expired = *password_expired; | |
| } | |
| // Get the user's privileges from the role | |
| redfish::Privileges user_privileges = redfish::getUserPrivileges(user_role); | |
| // Set isConfigureSelfOnly based on D-Bus results. This | |
| // ignores the results from both pamAuthenticateUser and the | |
| // value from any previous use of this session. | |
| req.session->isConfigureSelfOnly = expired; | |
| // Modify privileges if isConfigureSelfOnly. | |
| if (req.session->isConfigureSelfOnly) { | |
| // Remove all privileges except ConfigureSelf | |
| user_privileges = | |
| user_privileges.intersection(redfish::Privileges{"ConfigureSelf"}); | |
| BMCWEB_LOG_DEBUG << "Operation limited to ConfigureSelf"; | |
| } | |
| if (!rule.checkPrivileges(user_privileges)) { | |
| asyncResp->res.result(boost::beast::http::status::forbidden); | |
| return false; | |
| } | |
| req.userRole = user_role; | |
| return true; | |
| } | |
| void Trie::optimizeNode(Node* node) { | |
| for (size_t x : node->paramChildrens) { | |
| if (x == 0U) { | |
| continue; | |
| } | |
| Node* child = &nodes[x]; | |
| optimizeNode(child); | |
| } | |
| if (node->children.empty()) { | |
| return; | |
| } | |
| bool merge_with_child = true; | |
| for (const Node::ChildMap::value_type& kv : node->children) { | |
| Node* child = &nodes[kv.second]; | |
| if (!child->isSimpleNode()) { | |
| merge_with_child = false; | |
| break; | |
| } | |
| } | |
| if (merge_with_child) { | |
| Node::ChildMap merged; | |
| for (const Node::ChildMap::value_type& kv : node->children) { | |
| Node* child = &nodes[kv.second]; | |
| for (const Node::ChildMap::value_type& child_kv : child->children) { | |
| merged[kv.first + child_kv.first] = child_kv.second; | |
| } | |
| } | |
| node->children = std::move(merged); | |
| optimizeNode(node); | |
| } else { | |
| for (const Node::ChildMap::value_type& kv : node->children) { | |
| Node* child = &nodes[kv.second]; | |
| optimizeNode(child); | |
| } | |
| } | |
| } | |
| void Trie::findRouteIndexes(const std::string& reqUrl, | |
| std::vector<unsigned>& routeIndexes, | |
| const Node* node, unsigned pos) const { | |
| if (node == nullptr) { | |
| node = head(); | |
| } | |
| for (const Node::ChildMap::value_type& kv : node->children) { | |
| const std::string& fragment = kv.first; | |
| // NO_CDC: to be improved later. | |
| const Node* child = &nodes[kv.second]; | |
| if (pos >= reqUrl.size()) { | |
| if (child->ruleIndex != 0 && fragment != "/") { | |
| routeIndexes.push_back(child->ruleIndex); | |
| } | |
| findRouteIndexes(reqUrl, routeIndexes, child, | |
| static_cast<unsigned>(pos + fragment.size())); | |
| } else { | |
| if (reqUrl.compare(pos, fragment.size(), fragment) == 0) { | |
| findRouteIndexes(reqUrl, routeIndexes, child, | |
| static_cast<unsigned>(pos + fragment.size())); | |
| } | |
| } | |
| } | |
| } | |
| std::pair<unsigned int, RoutingParams> Trie::find(std::string_view reqUrl, | |
| const Node* node, size_t pos, | |
| RoutingParams* params) const { | |
| RoutingParams empty; | |
| if (params == nullptr) { | |
| params = ∅ | |
| } | |
| unsigned found{}; | |
| RoutingParams match_params; | |
| if (node == nullptr) { | |
| node = head(); | |
| } | |
| if (pos == reqUrl.size()) { | |
| return {node->ruleIndex, *params}; | |
| } | |
| auto update_found = [&found, | |
| &match_params](std::pair<unsigned, RoutingParams>& ret) { | |
| // Current Logic is to choose the rule with the lowest index in the allRules | |
| // vector | |
| bool replaceFound = found == 0U || found > ret.first; | |
| // With plugins enabled, we will return the first matched | |
| // found instead. This results in matching fixed URI segments whenever | |
| // possible relative to wildcards. | |
| #if defined(PLATFORM_PLUGINS_ENABLED) || \ | |
| defined(GOOGLE_COMMON_PLUGINS_ENABLED) || \ | |
| defined(GOOGLE_PLUGINS_INTERNAL_ENABLED) | |
| replaceFound = found == 0U; | |
| #endif | |
| if (ret.first != 0U && replaceFound) { | |
| found = ret.first; | |
| match_params = std::move(ret.second); | |
| } | |
| }; | |
| for (const Node::ChildMap::value_type& kv : node->children) { | |
| const std::string& fragment = kv.first; | |
| const Node* child = &nodes[kv.second]; | |
| if (reqUrl.compare(pos, fragment.size(), fragment) == 0) { | |
| std::pair<unsigned, RoutingParams> ret = | |
| find(reqUrl, child, pos + fragment.size(), params); | |
| update_found(ret); | |
| } | |
| } | |
| if (node->paramChildrens[static_cast<size_t>(ParamType::INT)] != 0U) { | |
| char c = reqUrl[pos]; | |
| if ((c >= '0' && c <= '9') || c == '+' || c == '-') { | |
| char* eptr = nullptr; | |
| errno = 0; | |
| int64_t value = std::strtoll(reqUrl.data() + pos, &eptr, 10); // NOLINT | |
| if (errno != ERANGE && eptr != reqUrl.data() + pos) { | |
| params->intParams.push_back(value); | |
| std::pair<unsigned, RoutingParams> ret = find( | |
| reqUrl, | |
| &nodes[node->paramChildrens[static_cast<size_t>(ParamType::INT)]], | |
| static_cast<size_t>(eptr - reqUrl.data()), params); | |
| update_found(ret); | |
| params->intParams.pop_back(); | |
| } | |
| } | |
| } | |
| if (node->paramChildrens[static_cast<size_t>(ParamType::UINT)] != 0U) { | |
| char c = reqUrl[pos]; | |
| if ((c >= '0' && c <= '9') || c == '+') { | |
| char* eptr = nullptr; | |
| errno = 0; | |
| uint64_t value = std::strtoull(reqUrl.data() + pos, &eptr, 10); // NOLINT | |
| if (errno != ERANGE && eptr != reqUrl.data() + pos) { | |
| params->uintParams.push_back(value); | |
| std::pair<unsigned, RoutingParams> ret = find( | |
| reqUrl, | |
| &nodes[node->paramChildrens[static_cast<size_t>(ParamType::UINT)]], | |
| static_cast<size_t>(eptr - reqUrl.data()), params); | |
| update_found(ret); | |
| params->uintParams.pop_back(); | |
| } | |
| } | |
| } | |
| if (node->paramChildrens[static_cast<size_t>(ParamType::DOUBLE)] != 0U) { | |
| char c = reqUrl[pos]; | |
| if ((c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.') { | |
| char* eptr = nullptr; | |
| errno = 0; | |
| double value = std::strtod(reqUrl.data() + pos, &eptr); | |
| if (errno != ERANGE && eptr != reqUrl.data() + pos) { | |
| params->doubleParams.push_back(value); | |
| std::pair<unsigned, RoutingParams> ret = | |
| find(reqUrl, | |
| &nodes[node->paramChildrens[static_cast<size_t>( | |
| ParamType::DOUBLE)]], | |
| static_cast<size_t>(eptr - reqUrl.data()), params); | |
| update_found(ret); | |
| params->doubleParams.pop_back(); | |
| } | |
| } | |
| } | |
| if (node->paramChildrens[static_cast<size_t>(ParamType::STRING)] != 0U) { | |
| size_t epos = pos; | |
| for (; epos < reqUrl.size(); epos++) { | |
| if (reqUrl[epos] == '/') { | |
| break; | |
| } | |
| } | |
| if (epos != pos) { | |
| params->stringParams.emplace_back(reqUrl.substr(pos, epos - pos)); | |
| std::pair<unsigned, RoutingParams> ret = find( | |
| reqUrl, | |
| &nodes[node->paramChildrens[static_cast<size_t>(ParamType::STRING)]], | |
| epos, params); | |
| update_found(ret); | |
| params->stringParams.pop_back(); | |
| } | |
| } | |
| if (node->paramChildrens[static_cast<size_t>(ParamType::PATH)] != 0U) { | |
| size_t epos = reqUrl.size(); | |
| if (epos != pos) { | |
| params->stringParams.emplace_back(reqUrl.substr(pos, epos - pos)); | |
| std::pair<unsigned, RoutingParams> ret = find( | |
| reqUrl, | |
| &nodes[node->paramChildrens[static_cast<size_t>(ParamType::PATH)]], | |
| epos, params); | |
| update_found(ret); | |
| params->stringParams.pop_back(); | |
| } | |
| } | |
| return {found, match_params}; | |
| } | |
| void Trie::add(const std::string& url, unsigned int ruleIndex) { | |
| size_t idx = 0; | |
| for (unsigned i = 0; i < url.size(); i++) { | |
| char c = url[i]; | |
| if (c == '<') { | |
| static const std::array<std::pair<ParamType, std::string>, 7> | |
| // NOLINTNEXTLINE | |
| 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) { | |
| if (url.compare(i, x.second.size(), x.second) == 0) { | |
| size_t index = static_cast<size_t>(x.first); | |
| if (nodes[idx].paramChildrens[index] == 0U) { | |
| unsigned new_node_idx = newNode(); | |
| nodes[idx].paramChildrens[index] = new_node_idx; | |
| } | |
| idx = nodes[idx].paramChildrens[index]; | |
| i += static_cast<unsigned>(x.second.size()); | |
| break; | |
| } | |
| } | |
| i--; | |
| } else { | |
| std::string piece(&c, 1); | |
| if (nodes[idx].children.count(piece) == 0U) { | |
| unsigned new_node_idx = newNode(); | |
| nodes[idx].children.emplace(piece, new_node_idx); | |
| } | |
| idx = nodes[idx].children[piece]; | |
| } | |
| } | |
| if (nodes[idx].ruleIndex != 0U) { | |
| throw std::runtime_error("handler already exists for " + url); | |
| } | |
| nodes[idx].ruleIndex = ruleIndex; | |
| } | |
| void Trie::debugNodePrint(Node* n, size_t level) | |
| { | |
| for (size_t i = 0; i < static_cast<size_t>(ParamType::MAX); i++) { | |
| if (n->paramChildrens[i] != 0U) { | |
| BMCWEB_LOG_DEBUG << std::string( | |
| 2U * level, ' ') /*<< "("<<n->paramChildrens[i]<<") "*/; | |
| switch (static_cast<ParamType>(i)) { | |
| case ParamType::INT: | |
| BMCWEB_LOG_DEBUG << "<int>"; | |
| break; | |
| case ParamType::UINT: | |
| BMCWEB_LOG_DEBUG << "<uint>"; | |
| break; | |
| case ParamType::DOUBLE: | |
| BMCWEB_LOG_DEBUG << "<float>"; | |
| break; | |
| case ParamType::STRING: | |
| BMCWEB_LOG_DEBUG << "<str>"; | |
| break; | |
| case ParamType::PATH: | |
| BMCWEB_LOG_DEBUG << "<path>"; | |
| break; | |
| case ParamType::MAX: | |
| BMCWEB_LOG_DEBUG << "<ERROR>"; | |
| break; | |
| } | |
| debugNodePrint(&nodes[n->paramChildrens[i]], level + 1); | |
| } | |
| } | |
| for (const Node::ChildMap::value_type& kv : n->children) { | |
| BMCWEB_LOG_DEBUG << std::string(2U * level, | |
| ' ') /*<< "(" << kv.second << ") "*/ | |
| << kv.first; | |
| debugNodePrint(&nodes[kv.second], level + 1); | |
| } | |
| } | |
| void Router::internalAddRuleObject(const std::string& rule, | |
| BaseRule* ruleObject) { | |
| if (ruleObject == nullptr) { | |
| return; | |
| } | |
| for (size_t method = 0, method_bit = 1; method <= methodNotAllowedIndex; | |
| method++, method_bit <<= 1) { | |
| if ((ruleObject->methodsBitfield & method_bit) > 0U) { | |
| perMethods[method].rules.push_back(ruleObject); | |
| perMethods[method].trie.add( | |
| rule, static_cast<unsigned>(perMethods[method].rules.size() - 1U)); | |
| // 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), | |
| static_cast<unsigned>(perMethods[method].rules.size() - 1)); | |
| } | |
| } | |
| } | |
| } | |
| void Router::validate() { | |
| for (std::unique_ptr<BaseRule>& rule : allRules) { | |
| if (rule) { | |
| std::unique_ptr<BaseRule> upgraded = rule->upgrade(); | |
| if (upgraded) { | |
| rule = std::move(upgraded); | |
| } | |
| rule->validate(); | |
| internalAddRuleObject(rule->rule, rule.get()); | |
| } | |
| } | |
| for (PerMethod& per_method : perMethods) { | |
| per_method.trie.validate(); | |
| } | |
| } | |
| Router::FindRoute Router::findRouteByIndex(std::string_view url, | |
| size_t index) const { | |
| FindRoute route; | |
| if (index >= perMethods.size()) { | |
| BMCWEB_LOG_CRITICAL << "Bad index???"; | |
| return route; | |
| } | |
| const PerMethod& per_method = perMethods[index]; | |
| std::pair<unsigned, RoutingParams> found = per_method.trie.find(url); | |
| if (found.first >= per_method.rules.size()) { | |
| throw std::runtime_error("Trie internal structure corrupted!"); | |
| } | |
| // Found a 404 route, switch that in | |
| if (found.first != 0U) { | |
| route.rule = per_method.rules[found.first]; | |
| route.params = std::move(found.second); | |
| } | |
| return route; | |
| } | |
| Router::FindRouteResponse Router::findRoute(Request& req) const { | |
| FindRouteResponse find_route; | |
| std::optional<HttpVerb> verb = httpVerbFromBoost(req.method()); | |
| if (!verb) { | |
| return find_route; | |
| } | |
| size_t req_method_index = static_cast<size_t>(*verb); | |
| // Check to see if this url exists at any verb | |
| for (size_t per_method_index = 0; per_method_index <= maxVerbIndex; | |
| per_method_index++) { | |
| // Make sure it's safe to deference the array at that index | |
| static_assert(maxVerbIndex < std::tuple_size_v<decltype(perMethods)>); | |
| FindRoute route = | |
| findRouteByIndex(req.url().encoded_path(), per_method_index); | |
| if (route.rule == nullptr) { | |
| continue; | |
| } | |
| if (!find_route.allowHeader.empty()) { | |
| find_route.allowHeader += ", "; | |
| } | |
| HttpVerb this_verb = static_cast<HttpVerb>(per_method_index); | |
| find_route.allowHeader += httpVerbToString(this_verb); | |
| if (per_method_index == req_method_index) { | |
| find_route.route = route; | |
| } | |
| } | |
| return find_route; | |
| } | |
| bool Router::isAccessAllowedFromInsecureTransport(const Request& req, | |
| std::string_view url_str) { | |
| return ::milotic::authz::BmcWebAuthorizerSingleton::GetInstance() | |
| .IsAccessAllowedFromInsecureTransport(url_str, req.method()); | |
| } | |
| bool Router::needsAuthorization(const Request& req, std::string_view url_str, | |
| bool restrict_transport, bool disable_authz) { | |
| // Always require authorization if req.fromGrpc is true. | |
| if (req.fromGrpc) { | |
| return true; | |
| } | |
| // We know its insecure and if we are not restricting redfish routes from | |
| // insecure transport, no need to authorize | |
| if (!restrict_transport) { | |
| return false; | |
| } | |
| // if authenticated and configured to skip authz, then skip authz. | |
| if (disable_authz && req.peer_authenticated) { | |
| return false; | |
| } | |
| // Otherwise, require authorization if access is not allowed from insecure | |
| // transport. | |
| return !isAccessAllowedFromInsecureTransport(req, url_str); | |
| } | |
| void Router::handle(Request& req, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { | |
| std::optional<HttpVerb> verb = httpVerbFromBoost(req.method()); | |
| if (!verb || static_cast<size_t>(*verb) >= perMethods.size()) { | |
| asyncResp->res.result(boost::beast::http::status::not_found); | |
| return; | |
| } | |
| FindRouteResponse found_route = findRoute(req); | |
| if (found_route.route.rule == nullptr && catch_all_rule_ == nullptr) { | |
| // Couldn't find a normal route with any verb, try looking for a 404 | |
| // route | |
| if (found_route.allowHeader.empty()) { | |
| found_route.route = | |
| findRouteByIndex(req.url().encoded_path(), notFoundIndex); | |
| } else { | |
| // See if we have a method not allowed (405) handler | |
| found_route.route = | |
| findRouteByIndex(req.url().encoded_path(), methodNotAllowedIndex); | |
| } | |
| } else if (found_route.route.rule == nullptr) { | |
| // We found a catch all rule, so we should use that | |
| found_route.route.rule = catch_all_rule_.get(); | |
| } | |
| // Fill in the allow header if it's valid | |
| if (!found_route.allowHeader.empty()) { | |
| asyncResp->res.addHeader(boost::beast::http::field::allow, | |
| found_route.allowHeader); | |
| } | |
| // If we couldn't find a real route or a 404 route, return a generic | |
| // response | |
| if (found_route.route.rule == nullptr) { | |
| if (found_route.allowHeader.empty()) { | |
| asyncResp->res.result(boost::beast::http::status::not_found); | |
| } else { | |
| asyncResp->res.result(boost::beast::http::status::method_not_allowed); | |
| } | |
| return; | |
| } | |
| BaseRule& rule = *found_route.route.rule; | |
| RoutingParams params = std::move(found_route.route.params); | |
| BMCWEB_LOG_INFO << "Matched rule '" << rule.rule << "' " | |
| << static_cast<uint32_t>(*verb) << " / " << rule.getMethods(); | |
| #ifdef BMCWEB_ENABLE_GRPC | |
| std::string_view url_str = {req.url().encoded_path().data(), | |
| req.url().encoded_path().size()}; | |
| if (needsAuthorization(req, req.url().encoded_path(), | |
| restrictRedfishRoutesFromInsecureTransport, | |
| insecureDisableGrpcRedfishAuthz)) { | |
| // Handles dynamic fine-grained authorization | |
| ::milotic::authz::BmcWebAuthorizerSingleton::RequestState authz_state; | |
| authz_state.with_trust_bundle = req.with_trust_bundle; | |
| authz_state.peer_authenticated = req.peer_authenticated; | |
| authz_state.peer_privileges = req.peer_privileges; | |
| authz_state.from_grpc = req.fromGrpc; | |
| grpc::Status status = | |
| ::milotic::authz::BmcWebAuthorizerSingleton::GetInstance().Authorize( | |
| url_str, req.method(), authz_state); | |
| if (!status.ok()) { | |
| asyncResp->res.addHeader("OData-Version", "4.0"); | |
| LOG(WARNING) << "Authorization failure at " << req.url() << ": " | |
| << status.error_message(); | |
| ::redfish::messages::resourceAtUriUnauthorized(asyncResp->res, req.url(), | |
| status.error_message()); | |
| return; | |
| } | |
| } | |
| rule.handle(req, asyncResp, params); | |
| #else | |
| validatePrivilege(req, asyncResp, rule, | |
| [&rule, asyncResp, params](Request& thisReq) mutable { | |
| rule.handle(thisReq, asyncResp, params); | |
| }); | |
| #endif | |
| } | |
| void Router::debugPrint() { | |
| for (size_t i = 0; i < perMethods.size(); i++) { | |
| BMCWEB_LOG_DEBUG << boost::beast::http::to_string( | |
| static_cast<boost::beast::http::verb>(i)); | |
| perMethods[i].trie.debugPrint(); | |
| } | |
| } | |
| std::vector<const std::string*> Router::getRoutes(const std::string& parent) | |
| { | |
| std::vector<const std::string*> ret; | |
| for (const PerMethod& pm : perMethods) { | |
| std::vector<unsigned> x; | |
| pm.trie.findRouteIndexes(parent, x); | |
| for (unsigned index : x) { | |
| ret.push_back(&pm.rules[index]->rule); | |
| } | |
| } | |
| return ret; | |
| } | |
| void Router::throwHandlerNotFound(std::string_view url, | |
| boost::beast::http::verb boostVerb) { | |
| std::string error = "handler doesn't exist for "; | |
| error += url; | |
| error += ", method "; | |
| error += boostVerbToString(boostVerb); | |
| throw std::runtime_error(error); | |
| } | |
| } // namespace crow |