diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 8489a8b90..436ff8e10 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -9,3 +9,6 @@ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/apps/basic) # Add NoBuiltins app add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/apps/nobuiltins) + +# Add Programmability app +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/apps/programmability) diff --git a/samples/apps/basic/basic.cpp b/samples/apps/basic/basic.cpp index 25d8c741a..429b849e8 100644 --- a/samples/apps/basic/basic.cpp +++ b/samples/apps/basic/basic.cpp @@ -6,7 +6,6 @@ #include "ccf/common_auth_policies.h" #include "ccf/ds/hash.h" #include "ccf/http_query.h" -#include "ccf/js/registry.h" #include "ccf/json_handler.h" #include "ccf/version.h" @@ -14,62 +13,19 @@ #define FMT_HEADER_ONLY #include +using namespace std; using namespace nlohmann; namespace basicapp { - using RecordsMap = kv::Map>; - static constexpr auto PRIVATE_RECORDS = "basic.records"; + using RecordsMap = kv::Map>; + static constexpr auto PRIVATE_RECORDS = "records"; - // This sample shows the features of DynamicJSEndpointRegistry. This sample - // adds a PUT /app/custom_endpoints, which calls install_custom_endpoints(), - // after first authenticating the caller (user_data["isAdmin"] is true), to - // install custom JavaScript endpoints. - // PUT /app/custom_endpoints is logically equivalent to passing a set_js_app - // proposal in governance, except the application resides in the application - // space. - class BasicHandlers : public ccf::js::DynamicJSEndpointRegistry + class BasicHandlers : public ccf::UserEndpointRegistry { - private: - std::optional try_get_user_id( - ccf::endpoints::EndpointContext& ctx) - { - if ( - const auto* cose_ident = - ctx.try_get_caller()) - { - return cose_ident->user_id; - } - else if ( - const auto* cert_ident = - ctx.try_get_caller()) - { - return cert_ident->user_id; - } - return std::nullopt; - } - - std::span get_body(ccf::endpoints::EndpointContext& ctx) - { - if ( - const auto* cose_ident = - ctx.try_get_caller()) - { - return cose_ident->content; - } - else - { - return ctx.rpc_ctx->get_request_body(); - } - } - public: BasicHandlers(ccfapp::AbstractNodeContext& context) : - ccf::js::DynamicJSEndpointRegistry( - context, - "public:custom_endpoints" // Internal KV space will be under - // public:custom_endpoints.* - ) + ccf::UserEndpointRegistry(context) { openapi_info.title = "CCF Basic App"; openapi_info.description = @@ -149,293 +105,6 @@ namespace basicapp }; make_endpoint("/records", HTTP_POST, post, {ccf::user_cert_auth_policy}) .install(); - - // Restrict what KV maps the JS code can access. Here we make the - // PRIVATE_RECORDS map, written by the hardcoded C++ endpoints, - // read-only for JS code. Additionally, we reserve any map beginning - // with "basic." (public or private) as inaccessible for the JS code, in - // case we want to use it for the C++ app in future. - set_js_kv_namespace_restriction( - [](const std::string& map_name, std::string& explanation) - -> ccf::js::KVAccessPermissions { - if (map_name == PRIVATE_RECORDS) - { - explanation = fmt::format( - "The {} map is managed by C++ endpoints, so is read-only in " - "JS.", - PRIVATE_RECORDS); - return ccf::js::KVAccessPermissions::READ_ONLY; - } - - if ( - map_name.starts_with("public:basic.") || - map_name.starts_with("basic.")) - { - explanation = - "The 'basic.' prefix is reserved by the C++ endpoints for future " - "use."; - return ccf::js::KVAccessPermissions::ILLEGAL; - } - - return ccf::js::KVAccessPermissions::READ_WRITE; - }); - - auto put_custom_endpoints = [this](ccf::endpoints::EndpointContext& ctx) { - const auto user_id = try_get_user_id(ctx); - if (!user_id.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_UNAUTHORIZED, - ccf::errors::InternalError, - "Failed to get user id"); - return; - } - // Authorization Check - nlohmann::json user_data = nullptr; - auto result = get_user_data_v1(ctx.tx, user_id.value(), user_data); - if (result == ccf::ApiResult::InternalError) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get user data for user {}: {}", - user_id.value(), - ccf::api_result_to_str(result))); - return; - } - const auto is_admin_it = user_data.find("isAdmin"); - - // Not every user gets to define custom endpoints, only users with - // isAdmin - if ( - !user_data.is_object() || is_admin_it == user_data.end() || - !is_admin_it.value().get()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Only admins may access this endpoint."); - return; - } - // End of Authorization Check - - const auto bundle = get_body(ctx); - const auto j = nlohmann::json::parse(bundle.begin(), bundle.end()); - const auto parsed_bundle = j.get(); - - result = install_custom_endpoints_v1(ctx.tx, parsed_bundle); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to install endpoints: {}", - ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); - }; - - make_endpoint( - "/custom_endpoints", - HTTP_PUT, - put_custom_endpoints, - {ccf::user_cose_sign1_auth_policy, ccf::user_cert_auth_policy}) - .set_auto_schema() - .install(); - - auto get_custom_endpoints = [this](ccf::endpoints::EndpointContext& ctx) { - ccf::js::Bundle bundle; - - auto result = get_custom_endpoints_v1(bundle, ctx.tx); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get endpoints: {}", ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); - ctx.rpc_ctx->set_response_body(nlohmann::json(bundle).dump(2)); - }; - - make_endpoint( - "/custom_endpoints", - HTTP_GET, - get_custom_endpoints, - {ccf::empty_auth_policy}) - .set_auto_schema() - .install(); - - auto get_custom_endpoints_module = - [this](ccf::endpoints::EndpointContext& ctx) { - std::string module_name; - - { - const auto parsed_query = - http::parse_query(ctx.rpc_ctx->get_request_query()); - - std::string error; - if (!http::get_query_value( - parsed_query, "module_name", module_name, error)) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidQueryParameterValue, - std::move(error)); - return; - } - } - - std::string code; - - auto result = - get_custom_endpoint_module_v1(code, ctx.tx, module_name); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get module: {}", ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, - http::headervalues::contenttype::JAVASCRIPT); - ctx.rpc_ctx->set_response_body(std::move(code)); - }; - - make_endpoint( - "/custom_endpoints/modules", - HTTP_GET, - get_custom_endpoints_module, - {ccf::empty_auth_policy}) - .add_query_parameter("module_name") - .install(); - - auto patch_runtime_options = - [this](ccf::endpoints::EndpointContext& ctx) { - const auto user_id = try_get_user_id(ctx); - if (!user_id.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_UNAUTHORIZED, - ccf::errors::InternalError, - "Failed to get user id"); - return; - } - - // Authorization Check - nlohmann::json user_data = nullptr; - auto result = get_user_data_v1(ctx.tx, user_id.value(), user_data); - if (result == ccf::ApiResult::InternalError) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get user data for user {}: {}", - user_id.value(), - ccf::api_result_to_str(result))); - return; - } - const auto is_admin_it = user_data.find("isAdmin"); - - // Not every user gets to define custom endpoints, only users with - // isAdmin - if ( - !user_data.is_object() || is_admin_it == user_data.end() || - !is_admin_it.value().get()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Only admins may access this endpoint."); - return; - } - // End of Authorization Check - - // Implement patch semantics. - // - Fetch current options - ccf::JSRuntimeOptions options; - get_js_runtime_options_v1(options, ctx.tx); - - // - Convert current options to JSON - auto j_options = nlohmann::json(options); - - const auto body = get_body(ctx); - // - Parse argument as JSON body - const auto arg_body = nlohmann::json::parse(body.begin(), body.end()); - - // - Merge, to overwrite current options with anything from body. Note - // that nulls mean deletions, which results in resetting to a default - // value - j_options.merge_patch(arg_body); - - // - Parse patched options from JSON - options = j_options.get(); - - result = set_js_runtime_options_v1(ctx.tx, options); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to set options: {}", ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); - ctx.rpc_ctx->set_response_body(nlohmann::json(options).dump(2)); - }; - make_endpoint( - "/custom_endpoints/runtime_options", - HTTP_PATCH, - patch_runtime_options, - {ccf::user_cose_sign1_auth_policy, ccf::user_cert_auth_policy}) - .install(); - - auto get_runtime_options = [this](ccf::endpoints::EndpointContext& ctx) { - ccf::JSRuntimeOptions options; - - auto result = get_js_runtime_options_v1(options, ctx.tx); - if (result != ccf::ApiResult::OK) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_INTERNAL_SERVER_ERROR, - ccf::errors::InternalError, - fmt::format( - "Failed to get runtime options: {}", - ccf::api_result_to_str(result))); - return; - } - - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - ctx.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); - ctx.rpc_ctx->set_response_body(nlohmann::json(options).dump(2)); - }; - make_endpoint( - "/custom_endpoints/runtime_options", - HTTP_GET, - get_runtime_options, - {ccf::empty_auth_policy}) - .set_auto_schema() - .install(); } }; } @@ -447,4 +116,4 @@ namespace ccfapp { return std::make_unique(context); } -} +} \ No newline at end of file diff --git a/samples/apps/programmability/CMakeLists.txt b/samples/apps/programmability/CMakeLists.txt new file mode 100644 index 000000000..039e3657d --- /dev/null +++ b/samples/apps/programmability/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the Apache 2.0 License. + +cmake_minimum_required(VERSION 3.16) + +project(programmability LANGUAGES C CXX) + +option(USE_UNSAFE_VERSION "Use build with unsafe logging levels" OFF) + +set(CCF_PROJECT "ccf_${COMPILE_TARGET}") +if(USE_UNSAFE_VERSION) + set(CCF_PROJECT "${CCF_PROJECT}_unsafe") +endif() + +if(NOT TARGET "ccf") + find_package(${CCF_PROJECT} REQUIRED) +endif() + +add_ccf_app(programmability SRCS programmability.cpp) + +# Generate an ephemeral signing key +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/signing_key.pem + COMMAND openssl genrsa -out ${CMAKE_CURRENT_BINARY_DIR}/signing_key.pem -3 + 3072 +) +add_custom_target( + programmability_signing_key ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/signing_key.pem +) + +sign_app_library( + programmability.enclave ${CMAKE_CURRENT_SOURCE_DIR}/oe_sign.conf + ${CMAKE_CURRENT_BINARY_DIR}/signing_key.pem +) diff --git a/samples/apps/programmability/oe_sign.conf b/samples/apps/programmability/oe_sign.conf new file mode 100644 index 000000000..397bde50e --- /dev/null +++ b/samples/apps/programmability/oe_sign.conf @@ -0,0 +1,7 @@ +# Enclave settings: +NumHeapPages=100000 +NumStackPages=1024 +NumTCS=14 +ProductID=1 +SecurityVersion=1 +# The Debug setting is automatically inserted by sign_app_library in CMake, to build both debuggable and non-debuggable variants diff --git a/samples/apps/programmability/programmability.cpp b/samples/apps/programmability/programmability.cpp new file mode 100644 index 000000000..f2b2800cc --- /dev/null +++ b/samples/apps/programmability/programmability.cpp @@ -0,0 +1,454 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +// CCF +#include "ccf/app_interface.h" +#include "ccf/common_auth_policies.h" +#include "ccf/ds/hash.h" +#include "ccf/http_query.h" +#include "ccf/js/registry.h" +#include "ccf/json_handler.h" +#include "ccf/version.h" + +#include +#define FMT_HEADER_ONLY +#include + +using namespace nlohmann; + +namespace programmabilityapp +{ + using RecordsMap = kv::Map>; + static constexpr auto PRIVATE_RECORDS = "programmability.records"; + + // This sample shows the features of DynamicJSEndpointRegistry. This sample + // adds a PUT /app/custom_endpoints, which calls install_custom_endpoints(), + // after first authenticating the caller (user_data["isAdmin"] is true), to + // install custom JavaScript endpoints. + // PUT /app/custom_endpoints is logically equivalent to passing a set_js_app + // proposal in governance, except the application resides in the application + // space. + class ProgrammabilityHandlers : public ccf::js::DynamicJSEndpointRegistry + { + private: + std::optional try_get_user_id( + ccf::endpoints::EndpointContext& ctx) + { + if ( + const auto* cose_ident = + ctx.try_get_caller()) + { + return cose_ident->user_id; + } + else if ( + const auto* cert_ident = + ctx.try_get_caller()) + { + return cert_ident->user_id; + } + return std::nullopt; + } + + std::span get_body(ccf::endpoints::EndpointContext& ctx) + { + if ( + const auto* cose_ident = + ctx.try_get_caller()) + { + return cose_ident->content; + } + else + { + return ctx.rpc_ctx->get_request_body(); + } + } + + public: + ProgrammabilityHandlers(ccfapp::AbstractNodeContext& context) : + ccf::js::DynamicJSEndpointRegistry( + context, + "public:custom_endpoints" // Internal KV space will be under + // public:custom_endpoints.* + ) + { + openapi_info.title = "CCF Programmabilit App"; + openapi_info.description = + "Lightweight application demonstrating app-space JS programmability"; + openapi_info.document_version = "0.0.1"; + + // This app contains a few hard-coded C++ endpoints, writing to a + // C++-controlled table, to show that these can co-exist with JS endpoints + auto put = [this](ccf::endpoints::EndpointContext& ctx) { + std::string key; + std::string error; + if (!get_path_param( + ctx.rpc_ctx->get_request_path_params(), "key", key, error)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_NO_CONTENT, + ccf::errors::InvalidResourceName, + "Missing key"); + return; + } + + auto records_handle = ctx.tx.template rw(PRIVATE_RECORDS); + records_handle->put(key, ctx.rpc_ctx->get_request_body()); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + }; + make_endpoint( + "/records/{key}", HTTP_PUT, put, {ccf::user_cert_auth_policy}) + .set_forwarding_required(ccf::endpoints::ForwardingRequired::Never) + .install(); + + auto get = [this](ccf::endpoints::ReadOnlyEndpointContext& ctx) { + std::string key; + std::string error; + if (!get_path_param( + ctx.rpc_ctx->get_request_path_params(), "key", key, error)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_NO_CONTENT, + ccf::errors::InvalidResourceName, + "Missing key"); + return; + } + + auto records_handle = ctx.tx.template ro(PRIVATE_RECORDS); + auto record = records_handle->get(key); + + if (record.has_value()) + { + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::TEXT); + ctx.rpc_ctx->set_response_body(record.value()); + return; + } + + ctx.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::InvalidResourceName, + "No such key"); + }; + make_read_only_endpoint( + "/records/{key}", HTTP_GET, get, {ccf::user_cert_auth_policy}) + .set_forwarding_required(ccf::endpoints::ForwardingRequired::Never) + .install(); + + auto post = [this](ccf::endpoints::EndpointContext& ctx) { + const nlohmann::json body = + nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); + + const auto records = body.get>(); + + auto records_handle = ctx.tx.template rw(PRIVATE_RECORDS); + for (const auto& [key, value] : records) + { + const std::vector value_vec(value.begin(), value.end()); + records_handle->put(key, value_vec); + } + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + }; + make_endpoint("/records", HTTP_POST, post, {ccf::user_cert_auth_policy}) + .install(); + + // Restrict what KV maps the JS code can access. Here we make the + // PRIVATE_RECORDS map, written by the hardcoded C++ endpoints, + // read-only for JS code. Additionally, we reserve any map beginning + // with "programmability." (public or private) as inaccessible for the JS + // code, in case we want to use it for the C++ app in future. + set_js_kv_namespace_restriction( + [](const std::string& map_name, std::string& explanation) + -> ccf::js::KVAccessPermissions { + if (map_name == PRIVATE_RECORDS) + { + explanation = fmt::format( + "The {} map is managed by C++ endpoints, so is read-only in " + "JS.", + PRIVATE_RECORDS); + return ccf::js::KVAccessPermissions::READ_ONLY; + } + + if ( + map_name.starts_with("public:programmability.") || + map_name.starts_with("programmability.")) + { + explanation = + "The 'programmability.' prefix is reserved by the C++ endpoints " + "for future " + "use."; + return ccf::js::KVAccessPermissions::ILLEGAL; + } + + return ccf::js::KVAccessPermissions::READ_WRITE; + }); + + auto put_custom_endpoints = [this](ccf::endpoints::EndpointContext& ctx) { + const auto user_id = try_get_user_id(ctx); + if (!user_id.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_UNAUTHORIZED, + ccf::errors::InternalError, + "Failed to get user id"); + return; + } + // Authorization Check + nlohmann::json user_data = nullptr; + auto result = get_user_data_v1(ctx.tx, user_id.value(), user_data); + if (result == ccf::ApiResult::InternalError) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get user data for user {}: {}", + user_id.value(), + ccf::api_result_to_str(result))); + return; + } + const auto is_admin_it = user_data.find("isAdmin"); + + // Not every user gets to define custom endpoints, only users with + // isAdmin + if ( + !user_data.is_object() || is_admin_it == user_data.end() || + !is_admin_it.value().get()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::AuthorizationFailed, + "Only admins may access this endpoint."); + return; + } + // End of Authorization Check + + const auto bundle = get_body(ctx); + const auto j = nlohmann::json::parse(bundle.begin(), bundle.end()); + const auto parsed_bundle = j.get(); + + result = install_custom_endpoints_v1(ctx.tx, parsed_bundle); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to install endpoints: {}", + ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); + }; + + make_endpoint( + "/custom_endpoints", + HTTP_PUT, + put_custom_endpoints, + {ccf::user_cose_sign1_auth_policy, ccf::user_cert_auth_policy}) + .set_auto_schema() + .install(); + + auto get_custom_endpoints = [this](ccf::endpoints::EndpointContext& ctx) { + ccf::js::Bundle bundle; + + auto result = get_custom_endpoints_v1(bundle, ctx.tx); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get endpoints: {}", ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(nlohmann::json(bundle).dump(2)); + }; + + make_endpoint( + "/custom_endpoints", + HTTP_GET, + get_custom_endpoints, + {ccf::empty_auth_policy}) + .set_auto_schema() + .install(); + + auto get_custom_endpoints_module = + [this](ccf::endpoints::EndpointContext& ctx) { + std::string module_name; + + { + const auto parsed_query = + http::parse_query(ctx.rpc_ctx->get_request_query()); + + std::string error; + if (!http::get_query_value( + parsed_query, "module_name", module_name, error)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidQueryParameterValue, + std::move(error)); + return; + } + } + + std::string code; + + auto result = + get_custom_endpoint_module_v1(code, ctx.tx, module_name); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get module: {}", ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, + http::headervalues::contenttype::JAVASCRIPT); + ctx.rpc_ctx->set_response_body(std::move(code)); + }; + + make_endpoint( + "/custom_endpoints/modules", + HTTP_GET, + get_custom_endpoints_module, + {ccf::empty_auth_policy}) + .add_query_parameter("module_name") + .install(); + + auto patch_runtime_options = + [this](ccf::endpoints::EndpointContext& ctx) { + const auto user_id = try_get_user_id(ctx); + if (!user_id.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_UNAUTHORIZED, + ccf::errors::InternalError, + "Failed to get user id"); + return; + } + + // Authorization Check + nlohmann::json user_data = nullptr; + auto result = get_user_data_v1(ctx.tx, user_id.value(), user_data); + if (result == ccf::ApiResult::InternalError) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get user data for user {}: {}", + user_id.value(), + ccf::api_result_to_str(result))); + return; + } + const auto is_admin_it = user_data.find("isAdmin"); + + // Not every user gets to define custom endpoints, only users with + // isAdmin + if ( + !user_data.is_object() || is_admin_it == user_data.end() || + !is_admin_it.value().get()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::AuthorizationFailed, + "Only admins may access this endpoint."); + return; + } + // End of Authorization Check + + // Implement patch semantics. + // - Fetch current options + ccf::JSRuntimeOptions options; + get_js_runtime_options_v1(options, ctx.tx); + + // - Convert current options to JSON + auto j_options = nlohmann::json(options); + + const auto body = get_body(ctx); + // - Parse argument as JSON body + const auto arg_body = nlohmann::json::parse(body.begin(), body.end()); + + // - Merge, to overwrite current options with anything from body. Note + // that nulls mean deletions, which results in resetting to a default + // value + j_options.merge_patch(arg_body); + + // - Parse patched options from JSON + options = j_options.get(); + + result = set_js_runtime_options_v1(ctx.tx, options); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to set options: {}", ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(nlohmann::json(options).dump(2)); + }; + make_endpoint( + "/custom_endpoints/runtime_options", + HTTP_PATCH, + patch_runtime_options, + {ccf::user_cose_sign1_auth_policy, ccf::user_cert_auth_policy}) + .install(); + + auto get_runtime_options = [this](ccf::endpoints::EndpointContext& ctx) { + ccf::JSRuntimeOptions options; + + auto result = get_js_runtime_options_v1(options, ctx.tx); + if (result != ccf::ApiResult::OK) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_INTERNAL_SERVER_ERROR, + ccf::errors::InternalError, + fmt::format( + "Failed to get runtime options: {}", + ccf::api_result_to_str(result))); + return; + } + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(nlohmann::json(options).dump(2)); + }; + make_endpoint( + "/custom_endpoints/runtime_options", + HTTP_GET, + get_runtime_options, + {ccf::empty_auth_policy}) + .set_auto_schema() + .install(); + } + }; +} + +namespace ccfapp +{ + std::unique_ptr make_user_endpoints( + ccfapp::AbstractNodeContext& context) + { + return std::make_unique( + context); + } +} diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index 70a885ed3..e719e49f3 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -2124,7 +2124,7 @@ if __name__ == "__main__": cr.add( "app_space_js", run_app_space_js, - package="samples/apps/basic/libbasic", + package="samples/apps/programmability/libprogrammability", nodes=infra.e2e_args.max_nodes(cr.args, f=0), initial_user_count=4, initial_member_count=2, diff --git a/tests/programmability.py b/tests/programmability.py index 089846ce2..2345871ba 100644 --- a/tests/programmability.py +++ b/tests/programmability.py @@ -279,21 +279,21 @@ def test_custom_endpoints_kv_restrictions(network, args): r = c.post("/app/try_write", {"table": "public:my_js_table"}) assert r.status_code == http.HTTPStatus.OK.value, r.status_code - LOG.info("'basic.records' is a read-only table") - r = c.post("/app/try_read", {"table": "basic.records"}) + LOG.info("'programmability.records' is a read-only table") + r = c.post("/app/try_read", {"table": "programmability.records"}) assert r.status_code == http.HTTPStatus.OK.value, r.status_code - r = c.post("/app/try_write", {"table": "basic.records"}) + r = c.post("/app/try_write", {"table": "programmability.records"}) assert r.status_code == http.HTTPStatus.BAD_REQUEST.value, r.status_code - LOG.info("'basic.' is a forbidden namespace") - r = c.post("/app/try_read", {"table": "basic.foo"}) + LOG.info("'programmability.' is a forbidden namespace") + r = c.post("/app/try_read", {"table": "programmability.foo"}) assert r.status_code == http.HTTPStatus.BAD_REQUEST.value, r.status_code - r = c.post("/app/try_write", {"table": "basic.foo"}) + r = c.post("/app/try_write", {"table": "programmability.foo"}) assert r.status_code == http.HTTPStatus.BAD_REQUEST.value, r.status_code - r = c.post("/app/try_read", {"table": "public:basic.foo"}) + r = c.post("/app/try_read", {"table": "public:programmability.foo"}) assert r.status_code == http.HTTPStatus.BAD_REQUEST.value, r.status_code - r = c.post("/app/try_write", {"table": "public:basic.foo"}) + r = c.post("/app/try_write", {"table": "public:programmability.foo"}) assert r.status_code == http.HTTPStatus.BAD_REQUEST.value, r.status_code LOG.info("Cannot grant access to gov/internal tables") @@ -564,9 +564,9 @@ if __name__ == "__main__": cr = ConcurrentRunner() cr.add( - "basic", + "programmability", run, - package="samples/apps/basic/libbasic", + package="samples/apps/programmability/libprogrammability", js_app_bundle=None, nodes=infra.e2e_args.min_nodes(cr.args, f=0), initial_user_count=2,