blob: c43a46820ea9eab688136b00027ae9d0ef88b70c [file] [log] [blame]
#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);
if (req.session->isConfigureSelfOnly) {
redfish::messages::passwordChangeRequired(
asyncResp->res,
crow::utility::urlFromPieces("redfish", "v1", "AccountService",
"Accounts", req.session->username));
}
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 = &empty;
}
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;
// For platforms with plugins enabled, we will return the first matched
// found instead. This results in matching fixed URI segments whenever
// possible relative to wildcards.
#ifdef PLATFORM_PLUGINS_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;
}
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) {
// 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);
}
}
// 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
if (!req.fromGrpc ||
(insecureDisableGrpcRedfishAuthz && req.peer_authenticated)) {
rule.handle(req, asyncResp, params);
return;
}
// Handles dynamic fine-grained authorization
std::string_view url_str = {req.url().encoded_path().data(),
req.url().encoded_path().size()};
::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;
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