Create standalone `programmability` app, restore minimal `basic` app for perf testing (#6266)

This commit is contained in:
Eddy Ashton 2024-06-14 11:30:23 +01:00 коммит произвёл GitHub
Родитель 32a6a91839
Коммит c4f19a8c6b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
7 изменённых файлов: 516 добавлений и 348 удалений

Просмотреть файл

@ -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)

Просмотреть файл

@ -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 <fmt/format.h>
using namespace std;
using namespace nlohmann;
namespace basicapp
{
using RecordsMap = kv::Map<std::string, std::vector<uint8_t>>;
static constexpr auto PRIVATE_RECORDS = "basic.records";
using RecordsMap = kv::Map<string, std::vector<uint8_t>>;
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<ccf::UserId> try_get_user_id(
ccf::endpoints::EndpointContext& ctx)
{
if (
const auto* cose_ident =
ctx.try_get_caller<ccf::UserCOSESign1AuthnIdentity>())
{
return cose_ident->user_id;
}
else if (
const auto* cert_ident =
ctx.try_get_caller<ccf::UserCertAuthnIdentity>())
{
return cert_ident->user_id;
}
return std::nullopt;
}
std::span<const uint8_t> get_body(ccf::endpoints::EndpointContext& ctx)
{
if (
const auto* cose_ident =
ctx.try_get_caller<ccf::UserCOSESign1AuthnIdentity>())
{
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<bool>())
{
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<ccf::js::Bundle>();
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<ccf::js::Bundle, void>()
.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<void, ccf::js::Bundle>()
.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<std::string>("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<bool>())
{
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<ccf::JSRuntimeOptions>();
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<void, ccf::JSRuntimeOptions>()
.install();
}
};
}
@ -447,4 +116,4 @@ namespace ccfapp
{
return std::make_unique<basicapp::BasicHandlers>(context);
}
}
}

Просмотреть файл

@ -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
)

Просмотреть файл

@ -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

Просмотреть файл

@ -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 <charconv>
#define FMT_HEADER_ONLY
#include <fmt/format.h>
using namespace nlohmann;
namespace programmabilityapp
{
using RecordsMap = kv::Map<std::string, std::vector<uint8_t>>;
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<ccf::UserId> try_get_user_id(
ccf::endpoints::EndpointContext& ctx)
{
if (
const auto* cose_ident =
ctx.try_get_caller<ccf::UserCOSESign1AuthnIdentity>())
{
return cose_ident->user_id;
}
else if (
const auto* cert_ident =
ctx.try_get_caller<ccf::UserCertAuthnIdentity>())
{
return cert_ident->user_id;
}
return std::nullopt;
}
std::span<const uint8_t> get_body(ccf::endpoints::EndpointContext& ctx)
{
if (
const auto* cose_ident =
ctx.try_get_caller<ccf::UserCOSESign1AuthnIdentity>())
{
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<RecordsMap>(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<RecordsMap>(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<std::map<std::string, std::string>>();
auto records_handle = ctx.tx.template rw<RecordsMap>(PRIVATE_RECORDS);
for (const auto& [key, value] : records)
{
const std::vector<uint8_t> 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<bool>())
{
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<ccf::js::Bundle>();
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<ccf::js::Bundle, void>()
.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<void, ccf::js::Bundle>()
.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<std::string>("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<bool>())
{
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<ccf::JSRuntimeOptions>();
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<void, ccf::JSRuntimeOptions>()
.install();
}
};
}
namespace ccfapp
{
std::unique_ptr<ccf::endpoints::EndpointRegistry> make_user_endpoints(
ccfapp::AbstractNodeContext& context)
{
return std::make_unique<programmabilityapp::ProgrammabilityHandlers>(
context);
}
}

Просмотреть файл

@ -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,

Просмотреть файл

@ -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,