Skip to content

Commit 0cf2b74

Browse files
Allow governance to write to app tables (#7088)
Co-authored-by: Amaury Chamayou <[email protected]>
1 parent b08735f commit 0cf2b74

File tree

11 files changed

+65
-48
lines changed

11 files changed

+65
-48
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [6.0.8]
9+
10+
[6.0.8]: https://github.com/microsoft/CCF/releases/tag/ccf-6.0.8
11+
12+
### Changed
13+
14+
- The constitution's `apply()` function may now write directly to public application (ie - non-governance) tables. Note that this access is _write-only_, so these tables can still not be read from. (#7088)
15+
816
## [6.0.7]
917

1018
[6.0.7]: https://github.com/microsoft/CCF/releases/tag/ccf-6.0.7

doc/audit/read_write_restrictions.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ CCF ensures that governance audit is possible offline from a ledger, by consider
4949
- Governance code must never read from private tables. Doing so might make decisions which could not be reproduced from the ledger by an auditor (ie. without access to ledger secrets).
5050
- Governance code must never read from application tables. Doing so might produce dependencies on data which was not signed by a member.
5151
- Governance code running pre-approval must only have read access to tables, and never write.
52-
- Governance code should not write to application tables, which could be modified further outside of governance.
5352
- Application code must not modify governance tables, as it could do so without constitution approval.
5453

5554
.. note::
5655

5756
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.
57+
An additional restriction was present until v7.0: "Governance code should not write to application tables, which could be modified further outside of governance". This was determined to be too strict, and prevents governance directly bootstrapping (or correcting) application table state, so was removed.
5858

5959
..
6060
A link to this page is included in the CCF source code, and returned in error messages.
@@ -75,9 +75,9 @@ The possible access permissions are elaborated in the table below:
7575
+==========================+============+============+============+============+============+============+
7676
| Pre-approval governance | Read-only | None | Read-only | None | None | None |
7777
+--------------------------+------------+------------+------------+------------+------------+------------+
78-
| Post-approval governance | Read-only | None | Writeable | None | None | None |
78+
| Post-approval governance | Read-only | None | Read/Write | None | Write-Only | None |
7979
+--------------------------+------------+------------+------------+------------+------------+------------+
80-
| Application | Read-only | None | Read-only | None | Writeable | Writeable |
80+
| Application | Read-only | None | Read-only | None | Read/Write | Read/Write |
8181
+--------------------------+------------+------------+------------+------------+------------+------------+
8282

8383
Any violation of these restrictions (eg - calling ``set`` on a `Read-only` table, or ``has`` on a `None` table) results in an exception being thrown.

include/ccf/js/kv_access_permissions.h

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,18 @@ namespace ccf::js
88
{
99
enum class KVAccessPermissions
1010
{
11-
READ_WRITE,
12-
READ_ONLY,
13-
ILLEGAL
11+
ILLEGAL = 0,
12+
READ_ONLY = 1 << 0,
13+
WRITE_ONLY = 1 << 1,
14+
READ_WRITE = READ_ONLY | WRITE_ONLY
1415
};
16+
17+
inline KVAccessPermissions intersect_access_permissions(
18+
KVAccessPermissions l, KVAccessPermissions r)
19+
{
20+
/* This could use std::to_underlying from C++23 */
21+
using T = std::underlying_type_t<KVAccessPermissions>;
22+
const auto intersection = (T)l & (T)r;
23+
return KVAccessPermissions(intersection);
24+
}
1525
}

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "ccf"
7-
version = "6.0.7"
7+
version = "6.0.8"
88
authors = [
99
{ name="CCF Team", email="[email protected]" },
1010
]

samples/apps/programmability/programmability.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ namespace programmabilityapp
5959
const auto roles = roles_it->get<std::vector<std::string>>();
6060
for (const auto& role : roles)
6161
{
62-
auto role_handle =
63-
tx.ro<RoleSet>(fmt::format("public:ccf.gov.roles.{}", role));
62+
auto role_handle = tx.ro<RoleSet>(
63+
fmt::format("public:programmability.roles.{}", role));
6464
if (role_handle->contains(action))
6565
{
6666
return true;

samples/constitutions/roles/set_role_definition.js

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ class KVSet {
2121
clear() {
2222
this.#map.clear();
2323
}
24-
25-
asSetOfStrings() {
26-
let set = new Set();
27-
this.#map.forEach((_, key) => set.add(ccf.bufToJsonCompatible(key)));
28-
return set;
29-
}
3024
}
3125

3226
actions.set(
@@ -41,19 +35,14 @@ actions.set(
4135
},
4236
function (args) {
4337
let roleDefinition = new KVSet(
44-
ccf.kv[`public:ccf.gov.roles.${args.role}`],
38+
ccf.kv[`public:programmability.roles.${args.role}`],
4539
);
46-
let oldValues = roleDefinition.asSetOfStrings();
40+
41+
roleDefinition.clear();
42+
4743
let newValues = new Set(args.actions);
48-
for (const action of oldValues) {
49-
if (!newValues.has(action)) {
50-
roleDefinition.delete(ccf.jsonCompatibleToBuf(action));
51-
}
52-
}
5344
for (const action of newValues) {
54-
if (!oldValues.has(action)) {
55-
roleDefinition.add(ccf.jsonCompatibleToBuf(action));
56-
}
45+
roleDefinition.add(ccf.jsonCompatibleToBuf(action));
5746
}
5847
},
5948
),

src/js/extensions/ccf/kv.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,11 @@ namespace ccf::js::extensions
107107

108108
// Name-based policy cannot grant more access (eg - cannot change
109109
// Read-Only to Read-Write), can only make it more restricted
110-
if (proposed_permission > access_permission)
110+
const auto combined_permission = ccf::js::intersect_access_permissions(
111+
proposed_permission, access_permission);
112+
if (combined_permission != access_permission)
111113
{
112-
access_permission = proposed_permission;
114+
access_permission = combined_permission;
113115
explanation = proposed_explanation;
114116
}
115117
}

src/js/extensions/ccf/kv_helpers.h

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -347,54 +347,57 @@ namespace ccf::js::extensions::kvhelpers
347347
ARG_COUNT, \
348348
FUNC_FACTORY_METHOD, \
349349
SETTER_METHOD, \
350-
PERMISSION_ERROR_MIN, \
350+
PERMISSION_FLAGS, \
351351
HANDLE_GETTER) \
352352
do \
353353
{ \
354+
/* This could use std::to_underlying from C++23 */ \
355+
const auto permitted = \
356+
ccf::js::intersect_access_permissions( \
357+
access_permission, PERMISSION_FLAGS) != KVAccessPermissions::ILLEGAL; \
354358
auto fn_val = ctx.FUNC_FACTORY_METHOD( \
355-
access_permission >= PERMISSION_ERROR_MIN ? C_FUNC_NAME##_denied : \
356-
C_FUNC_NAME<HANDLE_GETTER>, \
359+
!permitted ? C_FUNC_NAME##_denied : C_FUNC_NAME<HANDLE_GETTER>, \
357360
JS_METHOD_NAME, \
358361
ARG_COUNT); \
359362
JS_CHECK_EXC(fn_val); \
360-
if (access_permission >= PERMISSION_ERROR_MIN) \
363+
if (!permitted) \
361364
{ \
362365
JS_CHECK_SET( \
363366
fn_val.set("_error_msg", ctx.new_string(permission_explanation))); \
364367
} \
365368
JS_CHECK_SET(view_val.SETTER_METHOD(JS_METHOD_NAME, std::move(fn_val))); \
366369
} while (0)
367370

368-
#define MAKE_RO_FUNCTION(C_FUNC_NAME, JS_METHOD_NAME, ARG_COUNT) \
371+
#define MAKE_READ_FUNCTION(C_FUNC_NAME, JS_METHOD_NAME, ARG_COUNT) \
369372
MAKE_FUNCTION( \
370373
C_FUNC_NAME, \
371374
JS_METHOD_NAME, \
372375
ARG_COUNT, \
373376
new_c_function, \
374377
set, \
375-
KVAccessPermissions::ILLEGAL, \
378+
KVAccessPermissions::READ_ONLY, \
376379
GetReadOnlyHandle)
377380

378-
#define MAKE_RW_FUNCTION(C_FUNC_NAME, JS_METHOD_NAME, ARG_COUNT) \
381+
#define MAKE_WRITE_FUNCTION(C_FUNC_NAME, JS_METHOD_NAME, ARG_COUNT) \
379382
MAKE_FUNCTION( \
380383
C_FUNC_NAME, \
381384
JS_METHOD_NAME, \
382385
ARG_COUNT, \
383386
new_c_function, \
384387
set, \
385-
KVAccessPermissions::READ_ONLY, \
388+
KVAccessPermissions::WRITE_ONLY, \
386389
GetWriteHandle)
387390

388-
MAKE_RO_FUNCTION(js_kv_map_has, "has", 1);
389-
MAKE_RO_FUNCTION(js_kv_map_get, "get", 1);
391+
MAKE_READ_FUNCTION(js_kv_map_has, "has", 1);
392+
MAKE_READ_FUNCTION(js_kv_map_get, "get", 1);
390393

391-
MAKE_RO_FUNCTION(js_kv_map_foreach, "forEach", 1);
392-
MAKE_RO_FUNCTION(
394+
MAKE_READ_FUNCTION(js_kv_map_foreach, "forEach", 1);
395+
MAKE_READ_FUNCTION(
393396
js_kv_get_version_of_previous_write, "getVersionOfPreviousWrite", 1);
394397

395-
MAKE_RW_FUNCTION(js_kv_map_set, "set", 2);
396-
MAKE_RW_FUNCTION(js_kv_map_delete, "delete", 1);
397-
MAKE_RW_FUNCTION(js_kv_map_clear, "clear", 0);
398+
MAKE_WRITE_FUNCTION(js_kv_map_set, "set", 2);
399+
MAKE_WRITE_FUNCTION(js_kv_map_delete, "delete", 1);
400+
MAKE_WRITE_FUNCTION(js_kv_map_clear, "clear", 0);
398401

399402
// This is a _getter_, subtly different from a read-only function
400403
MAKE_FUNCTION(
@@ -403,7 +406,7 @@ namespace ccf::js::extensions::kvhelpers
403406
0,
404407
new_getter_c_function,
405408
set_getter,
406-
KVAccessPermissions::ILLEGAL,
409+
KVAccessPermissions::READ_ONLY,
407410
GetReadOnlyHandle);
408411

409412
#undef MAKE_RW_FUNCTION

src/js/permissions_checks.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ namespace ccf::js
7878
{
7979
return KVAccessPermissions::READ_ONLY;
8080
}
81+
case (TxAccess::GOV_RW):
82+
{
83+
return KVAccessPermissions::WRITE_ONLY;
84+
}
8185
default:
8286
{
8387
return KVAccessPermissions::ILLEGAL;
@@ -99,7 +103,8 @@ namespace ccf::js
99103
{
100104
char const* table_kind = permission == KVAccessPermissions::READ_ONLY ?
101105
"read-only" :
102-
"inaccessible";
106+
(permission == KVAccessPermissions::WRITE_ONLY ? "write-only" :
107+
"inaccessible");
103108

104109
char const* exec_context = "unknown";
105110
switch (access)

src/js/test/js.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,10 @@ TEST_CASE("Check KV Map access")
155155
}
156156

157157
{
158-
INFO("Public applications tables cannot even be read");
158+
INFO("Public applications tables cannot be read, but can be written to");
159159
REQUIRE(
160160
check_kv_map_access(TxAccess::GOV_RW, public_app_table_name) ==
161-
KVAccessPermissions::ILLEGAL);
161+
KVAccessPermissions::WRITE_ONLY);
162162
}
163163

164164
{

0 commit comments

Comments
 (0)