Skip to content

Commit

Permalink
Add ability to further restrict which tables are accessed by JS code (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
eddyashton authored Jun 11, 2024
1 parent 9574741 commit 8fa0d0e
Show file tree
Hide file tree
Showing 16 changed files with 482 additions and 173 deletions.
5 changes: 5 additions & 0 deletions doc/audit/read_write_restrictions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ CCF ensures that governance audit is possible offline from a ledger, by consider

An important exemption here is that application code may still `read` from governance tables. This allows authentication, authorization, and metadata to be configured and controlled by governance, but affect the execution of application endpoints.

..
A link to this page is included in the CCF source code, and returned in error messages.
If this table is moved, make sure the source is updated in-sync.
(Ctrl+Shift+F: read_write_restrictions)
The possible access permissions are elaborated in the table below:

.. table:: KV permissions in different execution contexts
Expand Down
2 changes: 1 addition & 1 deletion include/ccf/js/core/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ namespace ccf::js::core
JSWrappedValue new_c_function(
JSCFunction* func, const char* name, int length) const;
JSWrappedValue new_getter_c_function(
JSCFunction* func, const char* name) const;
JSCFunction* func, const char* name, size_t arg_count = 0) const;

JSWrappedValue duplicate_value(JSValueConst original) const;

Expand Down
11 changes: 9 additions & 2 deletions include/ccf/js/extensions/ccf/kv.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
#pragma once

#include "ccf/js/extensions/extension_interface.h"
#include "ccf/tx.h"
#include "ccf/js/namespace_restrictions.h"

#include <memory>

namespace kv
{
class Tx;
}

namespace ccf::js::extensions
{
/**
Expand All @@ -21,7 +26,9 @@ namespace ccf::js::extensions

std::unique_ptr<Impl> impl;

KvExtension(kv::Tx* t);
ccf::js::NamespaceRestriction namespace_restriction;

KvExtension(kv::Tx* t, const ccf::js::NamespaceRestriction& nr = {});
~KvExtension();

void install(js::core::Context& ctx);
Expand Down
15 changes: 15 additions & 0 deletions include/ccf/js/kv_access_permissions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/js/core/context.h"

namespace ccf::js
{
enum class KVAccessPermissions
{
READ_WRITE,
READ_ONLY,
ILLEGAL
};
}
17 changes: 17 additions & 0 deletions include/ccf/js/namespace_restrictions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/js/kv_access_permissions.h"

#include <functional>
#include <string>

namespace ccf::js
{
// A function which calculates some access permission based on the given map
// name. Should also populate an explanation, which can be included in error
// messages if disallowed methods are accessed.
using NamespaceRestriction = std::function<KVAccessPermissions(
const std::string& map_name, std::string& explanation)>;
}
9 changes: 9 additions & 0 deletions include/ccf/js/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "ccf/js/bundle.h"
#include "ccf/js/core/context.h"
#include "ccf/js/interpreter_cache_interface.h"
#include "ccf/js/namespace_restrictions.h"
#include "ccf/tx.h"
#include "ccf/tx_id.h"

Expand Down Expand Up @@ -51,6 +52,8 @@ namespace ccf::js
std::string modules_quickjs_bytecode_map;
std::string runtime_options_map;

ccf::js::NamespaceRestriction namespace_restriction;

using PreExecutionHook = std::function<void(ccf::js::core::Context&)>;

void do_execute_request(
Expand Down Expand Up @@ -103,6 +106,12 @@ namespace ccf::js
ccf::ApiResult get_custom_endpoint_module_v1(
std::string& code, kv::ReadOnlyTx& tx, const std::string& module_name);

/**
* Pass a function to control which maps can be accessed by JS endpoints.
*/
void set_js_kv_namespace_restriction(
const ccf::js::NamespaceRestriction& restriction);

/**
* Set options to control JS execution. Some hard limits may be applied to
* bound any values specified here.
Expand Down
30 changes: 30 additions & 0 deletions samples/apps/basic/basic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,36 @@ 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& caller_identity =
ctx.template get_caller<ccf::UserCOSESign1AuthnIdentity>();
Expand Down
4 changes: 2 additions & 2 deletions src/js/core/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,10 @@ namespace ccf::js::core
}

JSWrappedValue Context::new_getter_c_function(
JSCFunction* func, const char* name) const
JSCFunction* func, const char* name, size_t arg_count) const
{
return wrap(JS_NewCFunction2(
ctx, func, name, 0, JS_CFUNC_getter, JS_CFUNC_getter_magic));
ctx, func, name, arg_count, JS_CFUNC_getter, JS_CFUNC_getter_magic));
}

JSWrappedValue Context::duplicate_value(JSValueConst original) const
Expand Down
18 changes: 15 additions & 3 deletions src/js/extensions/ccf/historical.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,23 @@ namespace ccf::js::extensions
LOG_TRACE_FMT(
"Looking for historical kv map '{}' at seqno {}", map_name, seqno);

// Ignore evaluated access permissions - all tables are read-only
const auto access_permission = MapAccessPermissions::READ_ONLY;
auto access_permission =
ccf::js::check_kv_map_access(jsctx.access, map_name);
std::string explanation =
ccf::js::explain_kv_map_access(access_permission, jsctx.access);

// If it's illegal, it stays illegal in historical lookup
if (access_permission != KVAccessPermissions::ILLEGAL)
{
// But otherwise, ignore evaluated access permissions - all tables are
// read-only in historical KV
access_permission = KVAccessPermissions::READ_ONLY;
explanation = "All tables are read-only during historical transaction.";
}

auto handle_val =
kvhelpers::create_kv_map_handle<get_map_handle_historical, nullptr>(
jsctx, map_name, access_permission);
jsctx, map_name, access_permission, explanation);
if (JS_IsException(handle_val))
{
return -1;
Expand Down
36 changes: 32 additions & 4 deletions src/js/extensions/ccf/kv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
#include "ccf/js/extensions/ccf/kv.h"

#include "ccf/js/core/context.h"
#include "ccf/tx.h"
#include "js/checks.h"
#include "js/extensions/ccf/kv_helpers.h"
#include "js/global_class_ids.h"
#include "js/map_access_permissions.h"
#include "js/permissions_checks.h"

#include <map>
#include <quickjs/quickjs.h>
Expand Down Expand Up @@ -85,11 +86,37 @@ namespace ccf::js::extensions
const auto map_name = jsctx.to_str(property).value_or("");
LOG_TRACE_FMT("Looking for kv map '{}'", map_name);

const auto access_permission =
auto extension = jsctx.get_extension<KvExtension>();
if (extension == nullptr)
{
LOG_FAIL_FMT("No KV extension available");
return -1;
}

auto access_permission =
ccf::js::check_kv_map_access(jsctx.access, map_name);
std::string explanation =
ccf::js::explain_kv_map_access(access_permission, jsctx.access);

if (extension->namespace_restriction != nullptr)
{
std::string proposed_explanation;
const auto proposed_permission =
extension->namespace_restriction(map_name, proposed_explanation);

// Name-based policy cannot grant more access (eg - cannot change
// Read-Only to Read-Write), can only make it more restricted
if (proposed_permission > access_permission)
{
access_permission = proposed_permission;
explanation = proposed_explanation;
}
}

auto handle_val =
kvhelpers::create_kv_map_handle<get_ro_map_handle, get_map_handle>(
jsctx, map_name, access_permission);
jsctx, map_name, access_permission, explanation);

if (JS_IsException(handle_val))
{
return -1;
Expand All @@ -102,7 +129,8 @@ namespace ccf::js::extensions
}
}

KvExtension::KvExtension(kv::Tx* t)
KvExtension::KvExtension(kv::Tx* t, const ccf::js::NamespaceRestriction& nr) :
namespace_restriction(nr)
{
impl = std::make_unique<KvExtension::Impl>(t);
}
Expand Down
Loading

0 comments on commit 8fa0d0e

Please sign in to comment.