Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "ros2_medkit_gateway/auth/auth_manager.hpp"
#include "ros2_medkit_gateway/config.hpp"
#include "ros2_medkit_gateway/http/error_codes.hpp"
#include "ros2_medkit_gateway/http/http_utils.hpp"
#include "ros2_medkit_gateway/models/entity_capabilities.hpp"
#include "ros2_medkit_gateway/models/entity_types.hpp"

Expand Down Expand Up @@ -151,13 +152,16 @@ class HandlerContext {
/**
* @brief Get information about any entity (Component, App, Area, Function)
*
* Searches through all entity types in order: Component, App, Area, Function.
* Returns the first match found.
* If expected_type is specified, searches ONLY in that collection.
* If expected_type is UNKNOWN (default), searches all types in order:
* Component, App, Area, Function - returns the first match found.
*
* @param entity_id Entity ID to look up
* @param expected_type Optional: restrict search to specific entity type
* @return EntityInfo with resolved details, or EntityInfo with UNKNOWN type if not found
*/
EntityInfo get_entity_info(const std::string & entity_id) const;
EntityInfo get_entity_info(const std::string & entity_id,
SovdEntityType expected_type = SovdEntityType::UNKNOWN) const;

/**
* @brief Validate that entity supports a resource collection
Expand All @@ -172,6 +176,25 @@ class HandlerContext {
static std::optional<std::string> validate_collection_access(const EntityInfo & entity,
ResourceCollection collection);

/**
* @brief Validate entity exists and matches expected route type
*
* Unified validation helper that:
* 1. Validates entity ID format
* 2. Looks up entity in the expected collection (based on route path)
* 3. Sends appropriate error responses:
* - 400 with "invalid-parameter" if ID format is invalid
* - 400 with "invalid-parameter" if entity exists but wrong type for route
* - 404 with "entity-not-found" if entity doesn't exist
*
* @param req HTTP request (used to extract expected type from path)
* @param res HTTP response (error responses sent here)
* @param entity_id Entity ID to validate
* @return EntityInfo if valid, std::nullopt if error response was sent
*/
std::optional<EntityInfo> validate_entity_for_route(const httplib::Request & req, httplib::Response & res,
const std::string & entity_id) const;

/**
* @brief Set CORS headers on response if origin is allowed
* @param res HTTP response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

#include <string>

#include "ros2_medkit_gateway/models/entity_types.hpp"

namespace ros2_medkit_gateway {

/// API version prefix for all endpoints
Expand All @@ -32,6 +34,52 @@ inline std::string api_path(const std::string & endpoint) {
return std::string(API_BASE_PATH) + endpoint;
}

/**
* @brief Extract expected entity type from request path
*
* Parses the URL path to determine which entity type the route expects.
* Used for semantic validation - ensuring /components/{id} only accepts
* component IDs, not app IDs.
*
* @param path Request path (e.g., "/api/v1/components/my_component/data")
* @return Expected entity type, or UNKNOWN if path doesn't match entity routes
*/
inline SovdEntityType extract_entity_type_from_path(const std::string & path) {
// Path format: /api/v1/{entity_type}/{id}/...
// Check for each entity type prefix after API base path
const std::string base = std::string(API_BASE_PATH) + "/";

// Require a segment boundary after the collection name:
// matches "/api/v1/components" or "/api/v1/components/...", but not "/api/v1/componentship".
auto matches_collection = [&path, &base](const std::string & collection) -> bool {
const std::string prefix = base + collection;
if (path.compare(0, prefix.size(), prefix) != 0) {
return false;
}
if (path.size() == prefix.size()) {
// Exact match: "/api/v1/{collection}"
return true;
}
// Require a '/' segment separator after the collection name
return path[prefix.size()] == '/';
};

if (matches_collection("components")) {
return SovdEntityType::COMPONENT;
}
if (matches_collection("apps")) {
return SovdEntityType::APP;
}
if (matches_collection("areas")) {
return SovdEntityType::AREA;
}
if (matches_collection("functions")) {
return SovdEntityType::FUNCTION;
}

return SovdEntityType::UNKNOWN;
}

/**
* @brief Fault status filter flags for fault listing endpoints
*/
Expand Down
92 changes: 28 additions & 64 deletions src/ros2_medkit_gateway/src/http/handlers/config_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "ros2_medkit_gateway/gateway_node.hpp"
#include "ros2_medkit_gateway/http/error_codes.hpp"
#include "ros2_medkit_gateway/http/http_utils.hpp"
#include "ros2_medkit_gateway/http/x_medkit.hpp"

using json = nlohmann::json;
Expand Down Expand Up @@ -220,23 +221,14 @@ void ConfigHandlers::handle_list_configurations(const httplib::Request & req, ht

entity_id = req.matches[1];

auto entity_validation = ctx_.validate_entity_id(entity_id);
if (!entity_validation) {
HandlerContext::send_error(res, StatusCode::BadRequest_400, ERR_INVALID_PARAMETER, "Invalid entity ID",
{{"details", entity_validation.error()}, {"entity_id", entity_id}});
return;
}

// First, verify that the entity actually exists in the cache
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto entity_ref = cache.find_entity(entity_id);
if (!entity_ref) {
HandlerContext::send_error(res, StatusCode::NotFound_404, ERR_ENTITY_NOT_FOUND, "Entity not found",
{{"entity_id", entity_id}});
return;
// Validate entity ID and type for this route
auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id);
if (!entity_opt) {
return; // Error response already sent
}

// Get aggregated configurations info for this entity
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto agg_configs = cache.get_entity_configurations(entity_id);

// If no nodes to query, return empty result
Expand Down Expand Up @@ -371,11 +363,10 @@ void ConfigHandlers::handle_get_configuration(const httplib::Request & req, http
entity_id = req.matches[1];
param_id = req.matches[2];

auto entity_validation = ctx_.validate_entity_id(entity_id);
if (!entity_validation) {
HandlerContext::send_error(res, StatusCode::BadRequest_400, ERR_INVALID_PARAMETER, "Invalid entity ID",
{{"details", entity_validation.error()}, {"entity_id", entity_id}});
return;
// Validate entity ID and type for this route
auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id);
if (!entity_opt) {
return; // Error response already sent
}

// Parameter ID may be prefixed with app_id: for aggregated configs
Expand All @@ -385,16 +376,8 @@ void ConfigHandlers::handle_get_configuration(const httplib::Request & req, http
return;
}

// Verify entity exists
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto entity_ref = cache.find_entity(entity_id);
if (!entity_ref) {
HandlerContext::send_error(res, StatusCode::NotFound_404, ERR_ENTITY_NOT_FOUND, "Entity not found",
{{"entity_id", entity_id}});
return;
}

// Get aggregated configurations info
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto agg_configs = cache.get_entity_configurations(entity_id);

if (agg_configs.nodes.empty()) {
Expand Down Expand Up @@ -508,6 +491,7 @@ void ConfigHandlers::handle_set_configuration(const httplib::Request & req, http
entity_id = req.matches[1];
param_id = req.matches[2];

// Validate entity_id format first
auto entity_validation = ctx_.validate_entity_id(entity_id);
if (!entity_validation) {
HandlerContext::send_error(res, StatusCode::BadRequest_400, ERR_INVALID_PARAMETER, "Invalid entity ID",
Expand All @@ -521,7 +505,7 @@ void ConfigHandlers::handle_set_configuration(const httplib::Request & req, http
return;
}

// Parse request body
// Parse request body before checking entity existence
json body;
try {
body = json::parse(req.body);
Expand All @@ -543,16 +527,14 @@ void ConfigHandlers::handle_set_configuration(const httplib::Request & req, http
return;
}

// Verify entity exists
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto entity_ref = cache.find_entity(entity_id);
if (!entity_ref) {
HandlerContext::send_error(res, StatusCode::NotFound_404, ERR_ENTITY_NOT_FOUND, "Entity not found",
{{"entity_id", entity_id}});
return;
// Now validate entity exists and matches route type
auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id);
if (!entity_opt) {
return; // Error response already sent
}

// Get aggregated configurations info
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto agg_configs = cache.get_entity_configurations(entity_id);

if (agg_configs.nodes.empty()) {
Expand Down Expand Up @@ -641,23 +623,14 @@ void ConfigHandlers::handle_delete_configuration(const httplib::Request & req, h
entity_id = req.matches[1];
param_id = req.matches[2];

auto entity_validation = ctx_.validate_entity_id(entity_id);
if (!entity_validation) {
HandlerContext::send_error(res, StatusCode::BadRequest_400, ERR_INVALID_PARAMETER, "Invalid entity ID",
{{"details", entity_validation.error()}, {"entity_id", entity_id}});
return;
}

// Verify entity exists
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto entity_ref = cache.find_entity(entity_id);
if (!entity_ref) {
HandlerContext::send_error(res, StatusCode::NotFound_404, ERR_ENTITY_NOT_FOUND, "Entity not found",
{{"entity_id", entity_id}});
return;
// Validate entity ID and type for this route
auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id);
if (!entity_opt) {
return; // Error response already sent
}

// Get aggregated configurations info
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto agg_configs = cache.get_entity_configurations(entity_id);

if (agg_configs.nodes.empty()) {
Expand Down Expand Up @@ -730,23 +703,14 @@ void ConfigHandlers::handle_delete_all_configurations(const httplib::Request & r

entity_id = req.matches[1];

auto entity_validation = ctx_.validate_entity_id(entity_id);
if (!entity_validation) {
HandlerContext::send_error(res, StatusCode::BadRequest_400, ERR_INVALID_PARAMETER, "Invalid entity ID",
{{"details", entity_validation.error()}, {"entity_id", entity_id}});
return;
}

// Verify entity exists
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto entity_ref = cache.find_entity(entity_id);
if (!entity_ref) {
HandlerContext::send_error(res, StatusCode::NotFound_404, ERR_ENTITY_NOT_FOUND, "Entity not found",
{{"entity_id", entity_id}});
return;
// Validate entity ID and type for this route
auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id);
if (!entity_opt) {
return; // Error response already sent
}

// Get aggregated configurations info
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto agg_configs = cache.get_entity_configurations(entity_id);

if (agg_configs.nodes.empty()) {
Expand Down
56 changes: 14 additions & 42 deletions src/ros2_medkit_gateway/src/http/handlers/data_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,15 @@ void DataHandlers::handle_list_data(const httplib::Request & req, httplib::Respo

entity_id = req.matches[1];

auto validation_result = ctx_.validate_entity_id(entity_id);
if (!validation_result) {
HandlerContext::send_error(res, StatusCode::BadRequest_400, ERR_INVALID_PARAMETER, "Invalid entity ID",
{{"details", validation_result.error()}, {"entity_id", entity_id}});
return;
}

// First, verify that the entity actually exists in the cache
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto entity_ref = cache.find_entity(entity_id);
if (!entity_ref) {
HandlerContext::send_error(res, StatusCode::NotFound_404, ERR_ENTITY_NOT_FOUND, "Entity not found",
{{"entity_id", entity_id}});
return;
// Validate entity ID and type for this route
auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id);
if (!entity_opt) {
return; // Error response already sent
}
auto entity_info = *entity_opt;

// Use unified cache method to get aggregated data
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto aggregated = cache.get_entity_data(entity_id);

// Get data access manager for type introspection
Expand Down Expand Up @@ -129,20 +121,10 @@ void DataHandlers::handle_get_data_item(const httplib::Request & req, httplib::R
// cpp-httplib automatically decodes percent-encoded characters in URL path
topic_name = req.matches[2];

auto validation_result = ctx_.validate_entity_id(entity_id);
if (!validation_result) {
HandlerContext::send_error(res, StatusCode::BadRequest_400, ERR_INVALID_PARAMETER, "Invalid entity ID",
{{"details", validation_result.error()}, {"entity_id", entity_id}});
return;
}

// Verify entity exists
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto entity_ref = cache.find_entity(entity_id);
if (!entity_ref) {
HandlerContext::send_error(res, StatusCode::NotFound_404, ERR_ENTITY_NOT_FOUND, "Entity not found",
{{"entity_id", entity_id}});
return;
// Validate entity ID and type for this route
auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id);
if (!entity_opt) {
return; // Error response already sent
}

// Determine the full ROS topic path
Expand Down Expand Up @@ -221,11 +203,10 @@ void DataHandlers::handle_put_data_item(const httplib::Request & req, httplib::R
entity_id = req.matches[1];
topic_name = req.matches[2];

auto validation_result = ctx_.validate_entity_id(entity_id);
if (!validation_result) {
HandlerContext::send_error(res, StatusCode::BadRequest_400, ERR_INVALID_PARAMETER, "Invalid entity ID",
{{"details", validation_result.error()}, {"entity_id", entity_id}});
return;
// Validate entity ID and type for this route
auto entity_opt = ctx_.validate_entity_for_route(req, res, entity_id);
if (!entity_opt) {
return; // Error response already sent
}

// Parse request body
Expand Down Expand Up @@ -268,15 +249,6 @@ void DataHandlers::handle_put_data_item(const httplib::Request & req, httplib::R
return;
}

// Verify entity exists
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto entity_ref = cache.find_entity(entity_id);
if (!entity_ref) {
HandlerContext::send_error(res, StatusCode::NotFound_404, ERR_ENTITY_NOT_FOUND, "Entity not found",
{{"entity_id", entity_id}});
return;
}

// Build full topic path (mirror GET logic: only prefix '/' when needed)
std::string full_topic_path = topic_name;
if (!full_topic_path.empty() && full_topic_path.front() != '/') {
Expand Down
Loading