Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to further restrict which tables are accessed by JS code #6240

Merged
merged 25 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
34df485
Add a free function for checking namespace KV restrictions
eddyashton Jun 6, 2024
63f063c
Unit test the free function
eddyashton Jun 6, 2024
8574ad6
Move files around so we can plumb restrictions into the KV extension
eddyashton Jun 7, 2024
59c9be1
Add some demo restrictions to the basic app
eddyashton Jun 7, 2024
cf81369
Actually apply the restrictions
eddyashton Jun 7, 2024
130c09d
Add test_custom_endpoints_kv_restrictions
eddyashton Jun 7, 2024
0e48ba4
Move JS source to its own file, for readability
eddyashton Jun 7, 2024
cbd3daa
Pertier
eddyashton Jun 7, 2024
444ebc9
Merge branch 'main' of github.com:microsoft/CCF into js_kv_namespace_…
eddyashton Jun 7, 2024
11245cf
Reorg for some message customisation, but not enough...
eddyashton Jun 7, 2024
a06166e
Back to the drawing board - just pass a functor, so you can explain e…
eddyashton Jun 7, 2024
6d60496
Docstring
eddyashton Jun 7, 2024
194ab1e
Consistent explanations for historical access too
eddyashton Jun 7, 2024
302da52
Merge branch 'main' of github.com:microsoft/CCF into js_kv_namespace_…
eddyashton Jun 7, 2024
1c77948
Confirm this doesn't let us grant more powerful permissions!
eddyashton Jun 7, 2024
e1ec9f3
Leak less
eddyashton Jun 10, 2024
c708bd2
Merge branch 'main' of github.com:microsoft/CCF into js_kv_namespace_…
eddyashton Jun 10, 2024
ee46f35
Rename
eddyashton Jun 10, 2024
3ecddb7
Add back-reference comment to .rst
eddyashton Jun 10, 2024
ebfd808
Prefer map in new docs
eddyashton Jun 10, 2024
d69e9f0
Merge branch 'main' of github.com:microsoft/CCF into js_kv_namespace_…
eddyashton Jun 10, 2024
d1b6290
Oops
eddyashton Jun 10, 2024
5a70487
getters need to be set by set_getter not set
eddyashton Jun 11, 2024
0a4c278
Unsaved log remova
eddyashton Jun 11, 2024
eff6622
Merge branch 'main' of github.com:microsoft/CCF into js_kv_namespace_…
eddyashton Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading