diff --git a/enterprise/index/enterprise_index.cc b/enterprise/index/enterprise_index.cc index e4bfefc4d..a60d58ae7 100644 --- a/enterprise/index/enterprise_index.cc +++ b/enterprise/index/enterprise_index.cc @@ -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()); + 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}; + 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)}); + 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 diff --git a/enterprise/index/include/sourcemeta/one/enterprise_index.h b/enterprise/index/include/sourcemeta/one/enterprise_index.h index 8cb82037b..9d0a4b070 100644 --- a/enterprise/index/include/sourcemeta/one/enterprise_index.h +++ b/enterprise/index/include/sourcemeta/one/enterprise_index.h @@ -1,6 +1,7 @@ #ifndef SOURCEMETA_ONE_ENTERPRISE_INDEX_H_ #define SOURCEMETA_ONE_ENTERPRISE_INDEX_H_ +#include #include #include #include @@ -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 diff --git a/src/build/include/sourcemeta/one/build_state.h b/src/build/include/sourcemeta/one/build_state.h index b29f58bff..af035c6fa 100644 --- a/src/build/include/sourcemeta/one/build_state.h +++ b/src/build/include/sourcemeta/one/build_state.h @@ -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; diff --git a/src/index/endpoints.h b/src/index/endpoints.h new file mode 100644 index 000000000..eeab0a1c1 --- /dev/null +++ b/src/index/endpoints.h @@ -0,0 +1,50 @@ +#ifndef SOURCEMETA_ONE_INDEX_ENDPOINTS_H +#define SOURCEMETA_ONE_INDEX_ENDPOINTS_H + +#include // 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 diff --git a/src/index/explorer.h b/src/index/explorer.h index 215cd6c9d..97f87f89b 100644 --- a/src/index/explorer.h +++ b/src/index/explorer.h @@ -1,6 +1,8 @@ #ifndef SOURCEMETA_ONE_INDEX_EXPLORER_H_ #define SOURCEMETA_ONE_INDEX_EXPLORER_H_ +#include "endpoints.h" + #include #include #include @@ -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 @@ -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( diff --git a/src/index/generators.h b/src/index/generators.h index 2e209bf92..4219fe6ab 100644 --- a/src/index/generators.h +++ b/src/index/generators.h @@ -1,6 +1,7 @@ #ifndef SOURCEMETA_ONE_INDEX_GENERATORS_H_ #define SOURCEMETA_ONE_INDEX_GENERATORS_H_ +#include "endpoints.h" #include "error.h" #include @@ -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 @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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 { diff --git a/src/index/rules.h b/src/index/rules.h index d629a05f9..502004941 100644 --- a/src/index/rules.h +++ b/src/index/rules.h @@ -286,8 +286,10 @@ inline constexpr DeltaRuleSet<13, 8, 5, 2> INDEX_RULES{ .filename = "search.metapack"}, {.kind = ContainerDependencyKind::ExternalConfig, .filename = nullptr}, - {.kind = ContainerDependencyKind::Global, .filename = nullptr}}}, - .dependency_count = 3}, + {.kind = ContainerDependencyKind::Global, .filename = nullptr}, + {.kind = ContainerDependencyKind::Global, + .filename = "authentication.bin"}}}, + .dependency_count = 4}, {.action = ACTION_WEB_INDEX, .base = 1,