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
64 changes: 64 additions & 0 deletions enterprise/index/enterprise_index.cc
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,68 @@ auto generate_mcp_tools(const sourcemeta::core::URITemplateRouterView &router,
}
}

// TODO: Compose this through a proper RFC 9728 implementation in Core, rather
// than assembling the few fields this instance happens to need by hand
auto generate_protected_resource_metadata(
const sourcemeta::one::Authentication &authentication,
const sourcemeta::one::Configuration &configuration,
const std::string_view endpoint, sourcemeta::core::JSON &result) -> void {
std::string resource{configuration.url};
if (!resource.empty() && resource.back() == '/') {
resource.pop_back();
}

resource.append(endpoint);

// RFC 9728 Section 1.2 defines a resource identifier as an https URL, and a
// client is entitled to reject anything else. Loopback is the exception this
// project already makes elsewhere, so that a local instance stays testable
const sourcemeta::core::URI resource_uri{resource};
if (!resource_uri.is_https() &&
!(resource_uri.is_http() &&
(resource_uri.is_loopback() || resource_uri.is_localhost()))) {
return;
}

// A client that reads this asks its provider for a token bound to the
// resource below, so an issuer whose policy expects a different audience
// would mint one this instance refuses. Only an issuer whose policy accepts
// that audience can be named without sending the client into a rejection
auto servers{sourcemeta::core::JSON::make_array()};
for (const auto index : authentication.governing(
sourcemeta::one::Authentication::Path::relative(endpoint))) {
assert(index < configuration.authentication.size());
Comment thread
jviotti marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This bounds check relies solely on assert, which is compiled out in release builds. If authentication.bin is malformed or out of sync with the configuration (e.g., due to a corrupted artifact), index could be out-of-bounds and configuration.authentication[index] would read out-of-bounds memory, causing undefined behavior or a crash. Consider adding an explicit runtime bounds check that skips invalid indices instead of relying only on assert.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At enterprise/index/enterprise_index.cc, line 182:

<comment>This bounds check relies solely on `assert`, which is compiled out in release builds. If `authentication.bin` is malformed or out of sync with the configuration (e.g., due to a corrupted artifact), `index` could be out-of-bounds and `configuration.authentication[index]` would read out-of-bounds memory, causing undefined behavior or a crash. Consider adding an explicit runtime bounds check that skips invalid indices instead of relying only on `assert`.</comment>

<file context>
@@ -161,4 +161,48 @@ auto generate_mcp_tools(const sourcemeta::core::URITemplateRouterView &router,
+  auto servers{sourcemeta::core::JSON::make_array()};
+  for (const auto index : authentication.governing(
+           sourcemeta::one::Authentication::Path::relative(endpoint))) {
+    assert(index < configuration.authentication.size());
+    const auto &entry{configuration.authentication[index]};
+    if (entry.type !=
</file context>
Suggested change
assert(index < configuration.authentication.size());
if (index >= configuration.authentication.size()) {
continue;
}

const auto &entry{configuration.authentication[index]};
if (entry.type !=
sourcemeta::one::Configuration::AuthenticationEntry::Type::JWT ||
entry.audience != resource) {
continue;
}

// A policy that names its keys outright is never asked to discover
// anything, so nothing has established that its issuer is the https
// identifier RFC 8414 expects. Advertising it unchecked would publish an
// authorization server a client cannot use
if (!sourcemeta::core::URI{entry.issuer}.is_https()) {
continue;
}

sourcemeta::core::JSON issuer{entry.issuer};
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (!servers.contains(issuer)) {
servers.push_back(std::move(issuer));
}
}

if (servers.empty()) {
return;
}

result = sourcemeta::core::JSON::make_object();
result.assign("resource", sourcemeta::core::JSON{std::move(resource)});
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
result.assign("authorization_servers", std::move(servers));
auto bearer_methods{sourcemeta::core::JSON::make_array()};
bearer_methods.push_back(sourcemeta::core::JSON{"header"});
result.assign("bearer_methods_supported", std::move(bearer_methods));
}

} // namespace sourcemeta::one
9 changes: 9 additions & 0 deletions enterprise/index/include/sourcemeta/one/enterprise_index.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef SOURCEMETA_ONE_ENTERPRISE_INDEX_H_
#define SOURCEMETA_ONE_ENTERPRISE_INDEX_H_

#include <sourcemeta/one/authentication.h>
#include <sourcemeta/one/build.h>
#include <sourcemeta/one/configuration.h>
#include <sourcemeta/one/resolver.h>
Expand All @@ -27,6 +28,14 @@ auto generate_mcp_tools(const sourcemeta::core::URITemplateRouterView &router,
sourcemeta::core::JSON &tools,
sourcemeta::core::JSON &tool_routes) -> void;

// RFC 9728 metadata naming where a token for the MCP endpoint comes from, left
// untouched when no policy can honestly answer that
// https://datatracker.ietf.org/doc/html/rfc9728
auto generate_protected_resource_metadata(
const sourcemeta::one::Authentication &authentication,
const sourcemeta::one::Configuration &configuration,
std::string_view endpoint, sourcemeta::core::JSON &result) -> void;

} // namespace sourcemeta::one

#endif
2 changes: 1 addition & 1 deletion src/build/include/sourcemeta/one/build_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ struct ContainerDependency {
const char *filename;
};

inline constexpr std::size_t MAX_CONTAINER_DEPENDENCIES = 3;
inline constexpr std::size_t MAX_CONTAINER_DEPENDENCIES = 4;

struct ContainerRule {
BuildPlan::Action::Type action;
Expand Down
50 changes: 50 additions & 0 deletions src/index/endpoints.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#ifndef SOURCEMETA_ONE_INDEX_ENDPOINTS_H
#define SOURCEMETA_ONE_INDEX_ENDPOINTS_H

#include <string_view> // std::string_view

namespace sourcemeta::one {

// Every route this instance serves, as a URI template relative to the instance
// root. The router is populated from here, and whatever else has to name the
// same endpoint reads it from here rather than spelling it again

inline constexpr std::string_view ENDPOINT_LIST_DIRECTORY{
"/self/v1/api/list{/path*}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_DEPENDENCIES{
"/self/v1/api/schemas/dependencies/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_DEPENDENTS{
"/self/v1/api/schemas/dependents/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_HEALTH{
"/self/v1/api/schemas/health/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_LOCATIONS{
"/self/v1/api/schemas/locations/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_POSITIONS{
"/self/v1/api/schemas/positions/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_STATS{
"/self/v1/api/schemas/stats/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_METADATA{
"/self/v1/api/schemas/metadata/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_EVALUATE{
"/self/v1/api/schemas/evaluate/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_RDF{
"/self/v1/api/schemas/rdf/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_TRACE{
"/self/v1/api/schemas/trace/{+schema}"};
inline constexpr std::string_view ENDPOINT_SCHEMA_SEARCH{
"/self/v1/api/schemas/search"};
inline constexpr std::string_view ENDPOINT_HEALTH{"/self/v1/health"};
inline constexpr std::string_view ENDPOINT_AUTH_LOGOUT{"/self/v1/auth/logout"};
inline constexpr std::string_view ENDPOINT_AUTH_LOGIN{
"/self/v1/auth/login/{policy}"};
inline constexpr std::string_view ENDPOINT_AUTH_CALLBACK{
"/self/v1/auth/callback/{policy}"};
inline constexpr std::string_view ENDPOINT_MCP{"/self/v1/mcp"};
// Clients that normalise URLs by appending a slash reach the same handler
inline constexpr std::string_view ENDPOINT_MCP_TRAILING_SLASH{"/self/v1/mcp/"};
inline constexpr std::string_view ENDPOINT_API_NOT_FOUND{"/self/v1/api/{+any}"};
inline constexpr std::string_view ENDPOINT_STATIC{"/self/v1/static/{+path}"};

} // namespace sourcemeta::one

#endif
14 changes: 13 additions & 1 deletion src/index/explorer.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#ifndef SOURCEMETA_ONE_INDEX_EXPLORER_H_
#define SOURCEMETA_ONE_INDEX_EXPLORER_H_

#include "endpoints.h"

#include <sourcemeta/one/authentication.h>
#include <sourcemeta/one/configuration.h>
#include <sourcemeta/one/metapack.h>
Expand Down Expand Up @@ -691,12 +693,18 @@ struct GENERATE_MCP {

auto tools{sourcemeta::core::JSON::make_array()};
auto tool_routes{sourcemeta::core::JSON::make_object()};
auto protected_resource_metadata{sourcemeta::core::JSON{nullptr}};

#if defined(SOURCEMETA_ONE_ENTERPRISE)
{
const sourcemeta::core::URITemplateRouterView router_view{
action.dependencies.back()};
action.dependencies.at(2)};
sourcemeta::one::generate_mcp_tools(router_view, tools, tool_routes);
const sourcemeta::one::Authentication authentication{
action.dependencies.back(), {}};
sourcemeta::one::generate_protected_resource_metadata(
authentication, configuration, sourcemeta::one::ENDPOINT_MCP,
protected_resource_metadata);
}
#endif

Expand Down Expand Up @@ -734,6 +742,10 @@ struct GENERATE_MCP {
document.assign(std::string{sourcemeta::core::MCP_METHOD_TOOLS_LIST},
std::move(tools));
document.assign("toolRoutes", std::move(tool_routes));
if (!protected_resource_metadata.is_null()) {
document.assign("protectedResourceMetadata",
std::move(protected_resource_metadata));
}

const auto timestamp_end{std::chrono::steady_clock::now()};
sourcemeta::one::metapack_write_pretty_json(
Expand Down
64 changes: 32 additions & 32 deletions src/index/generators.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef SOURCEMETA_ONE_INDEX_GENERATORS_H_
#define SOURCEMETA_ONE_INDEX_GENERATORS_H_

#include "endpoints.h"
#include "error.h"

#include <sourcemeta/one/actions.h>
Expand Down Expand Up @@ -858,8 +859,8 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{list_directory_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/list{/path*}", "list_directory", next_id++,
sourcemeta::one::ACTION_TYPE_LIST_DIRECTORY_V1,
router.add(sourcemeta::one::ENDPOINT_LIST_DIRECTORY, "list_directory",
next_id++, sourcemeta::one::ACTION_TYPE_LIST_DIRECTORY_V1,
list_arguments);

const sourcemeta::core::URITemplateRouter::Argument
Expand All @@ -870,7 +871,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{get_schema_dependencies_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/dependencies/{+schema}",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_DEPENDENCIES,
"get_schema_dependencies", next_id++,
sourcemeta::one::ACTION_TYPE_GET_SCHEMA_DEPENDENCIES_V1,
dependencies_arguments);
Expand All @@ -883,7 +884,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{get_schema_dependents_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/dependents/{+schema}",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_DEPENDENTS,
"get_schema_dependents", next_id++,
sourcemeta::one::ACTION_TYPE_GET_SCHEMA_DEPENDENTS_V1,
dependents_arguments);
Expand All @@ -895,7 +896,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{get_schema_health_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/health/{+schema}", "get_schema_health",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_HEALTH, "get_schema_health",
next_id++, sourcemeta::one::ACTION_TYPE_GET_SCHEMA_HEALTH_V1,
health_arguments);

Expand All @@ -907,7 +908,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{get_schema_locations_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/locations/{+schema}",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_LOCATIONS,
"get_schema_locations", next_id++,
sourcemeta::one::ACTION_TYPE_GET_SCHEMA_LOCATIONS_V1,
locations_arguments);
Expand All @@ -920,7 +921,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{get_schema_positions_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/positions/{+schema}",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_POSITIONS,
"get_schema_positions", next_id++,
sourcemeta::one::ACTION_TYPE_GET_SCHEMA_POSITIONS_V1,
positions_arguments);
Expand All @@ -932,7 +933,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{get_schema_stats_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/stats/{+schema}", "get_schema_stats",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_STATS, "get_schema_stats",
next_id++, sourcemeta::one::ACTION_TYPE_GET_SCHEMA_STATS_V1,
stats_arguments);

Expand All @@ -943,7 +944,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{get_schema_metadata_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/metadata/{+schema}",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_METADATA,
"get_schema_metadata", next_id++,
sourcemeta::one::ACTION_TYPE_GET_SCHEMA_METADATA_V1,
metadata_arguments);
Expand All @@ -956,7 +957,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{evaluate_schema_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/evaluate/{+schema}", "evaluate_schema",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_EVALUATE, "evaluate_schema",
next_id++, sourcemeta::one::ACTION_TYPE_JSONSCHEMA_EVALUATE_V1,
evaluate_arguments);

Expand All @@ -968,7 +969,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{instance_to_rdf_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/rdf/{+schema}", "instance_to_rdf",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_RDF, "instance_to_rdf",
next_id++, sourcemeta::one::ACTION_TYPE_JSONSCHEMA_RDF_V1,
rdf_arguments);

Expand All @@ -980,7 +981,7 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{trace_schema_evaluation_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/trace/{+schema}",
router.add(sourcemeta::one::ENDPOINT_SCHEMA_TRACE,
"trace_schema_evaluation", next_id++,
sourcemeta::one::ACTION_TYPE_JSONSCHEMA_TRACE_V1,
trace_arguments);
Expand All @@ -991,67 +992,66 @@ struct GENERATE_URITEMPLATE_ROUTES {
{"mcpResponseSchema",
std::string_view{search_schemas_response_schema}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/schemas/search", "search_schemas", next_id++,
sourcemeta::one::ACTION_TYPE_SCHEMA_SEARCH_V1,
router.add(sourcemeta::one::ENDPOINT_SCHEMA_SEARCH, "search_schemas",
next_id++, sourcemeta::one::ACTION_TYPE_SCHEMA_SEARCH_V1,
search_arguments);

const sourcemeta::core::URITemplateRouter::Argument
health_check_arguments[] = {
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/health", "check_server_health", next_id++,
sourcemeta::one::ACTION_TYPE_HEALTH_CHECK_V1,
router.add(sourcemeta::one::ENDPOINT_HEALTH, "check_server_health",
next_id++, sourcemeta::one::ACTION_TYPE_HEALTH_CHECK_V1,
health_check_arguments);

const sourcemeta::core::URITemplateRouter::Argument
auth_logout_arguments[] = {
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/auth/logout", "auth_logout", next_id++,
sourcemeta::one::ACTION_TYPE_AUTH_LOGOUT_V1,
router.add(sourcemeta::one::ENDPOINT_AUTH_LOGOUT, "auth_logout",
next_id++, sourcemeta::one::ACTION_TYPE_AUTH_LOGOUT_V1,
auth_logout_arguments);

const sourcemeta::core::URITemplateRouter::Argument
auth_login_arguments[] = {
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/auth/login/{policy}", "auth_login", next_id++,
router.add(sourcemeta::one::ENDPOINT_AUTH_LOGIN, "auth_login", next_id++,
sourcemeta::one::ACTION_TYPE_AUTH_LOGIN_V1,
auth_login_arguments);

const sourcemeta::core::URITemplateRouter::Argument
auth_callback_arguments[] = {
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/auth/callback/{policy}", "auth_callback", next_id++,
sourcemeta::one::ACTION_TYPE_AUTH_CALLBACK_V1,
router.add(sourcemeta::one::ENDPOINT_AUTH_CALLBACK, "auth_callback",
next_id++, sourcemeta::one::ACTION_TYPE_AUTH_CALLBACK_V1,
auth_callback_arguments);

const sourcemeta::core::URITemplateRouter::Argument mcp_arguments[] = {
{"requestSchema", std::string_view{mcp_request_schema}},
{"responseSchema", std::string_view{mcp_response_schema}}};
router.add("/self/v1/mcp", "handle_mcp_request", next_id++,
router.add(sourcemeta::one::ENDPOINT_MCP, "handle_mcp_request", next_id++,
sourcemeta::one::ACTION_TYPE_MCP_V1, mcp_arguments);
router.add(sourcemeta::one::ENDPOINT_MCP_TRAILING_SLASH,
"handle_mcp_request_trailing_slash", next_id++,
sourcemeta::one::ACTION_TYPE_MCP_V1, mcp_arguments);
// Trailing-slash variant for clients that normalise URLs by
// appending `/`. Both routes dispatch to the same MCP handler
router.add("/self/v1/mcp/", "handle_mcp_request_trailing_slash",
next_id++, sourcemeta::one::ACTION_TYPE_MCP_V1, mcp_arguments);

const sourcemeta::core::URITemplateRouter::Argument
not_found_arguments[] = {
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/api/{+any}", "handle_not_found", next_id++,
sourcemeta::one::ACTION_TYPE_NOT_FOUND_V1,
router.add(sourcemeta::one::ENDPOINT_API_NOT_FOUND, "handle_not_found",
next_id++, sourcemeta::one::ACTION_TYPE_NOT_FOUND_V1,
not_found_arguments);

if (action.data == "Full") {
const sourcemeta::core::URITemplateRouter::Argument static_arguments[] =
{{"path", std::string_view{SOURCEMETA_ONE_STATIC}},
{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/static/{+path}", "serve_static_asset", next_id++,
sourcemeta::one::ACTION_TYPE_SERVE_STATIC_V1,
router.add(sourcemeta::one::ENDPOINT_STATIC, "serve_static_asset",
next_id++, sourcemeta::one::ACTION_TYPE_SERVE_STATIC_V1,
static_arguments);
} else {
const sourcemeta::core::URITemplateRouter::Argument static_arguments[] =
{{"errorSchema", std::string_view{error_schema}}};
router.add("/self/v1/static/{+path}", "serve_static_asset", next_id++,
sourcemeta::one::ACTION_TYPE_SERVE_STATIC_V1,
router.add(sourcemeta::one::ENDPOINT_STATIC, "serve_static_asset",
next_id++, sourcemeta::one::ACTION_TYPE_SERVE_STATIC_V1,
static_arguments);
}
} else {
Expand Down
Loading
Loading