зеркало из https://github.com/microsoft/CCF.git
Create standalone `programmability` app, restore minimal `basic` app for perf testing (#6266)
This commit is contained in:
Родитель
32a6a91839
Коммит
c4f19a8c6b
|
@ -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,
|
||||
|
|
Загрузка…
Ссылка в новой задаче