Skip to content

Commit 850122a

Browse files
authored
Test and document set_user_data (#1450)
1 parent f8c9d45 commit 850122a

File tree

8 files changed

+162
-5
lines changed

8 files changed

+162
-5
lines changed

doc/members/open_network.rst

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Then, the certificates of trusted users should be registered in CCF via the memb
2626
"state": "OPEN"
2727
}
2828
29-
Other members are then allowed to vote for the proposal, using the proposal id returned to the proposer member (here ``5``). They may submit an unconditional approval, or their vote may query the current state and the proposed calls. These votes `must` be signed.
29+
Other members are then allowed to vote for the proposal, using the proposal id returned to the proposer member (here ``5``). They may submit an unconditional approval, or their vote may query the current state and the proposed actions. These votes `must` be signed.
3030
3131
.. code-block:: bash
3232
@@ -69,7 +69,53 @@ The user can then make user RPCs, for example ``user_id`` to retrieve the unique
6969
"caller_id": 4
7070
}
7171
72-
For each user CCF also stores arbitrary user-data in a JSON object, which can only be written to by members, subject to the standard proposal-vote governance mechanism. This lets members define initial metadata for certain users; for example to grant specific privileges, associate a human-readable name, or categorise the users. This user-data can then be read (but not written) by user-facing apps.
72+
User Data
73+
---------
74+
75+
For each user, CCF also stores arbitrary user-data in a JSON object. This can only be written to by members, subject to the standard proposal-vote governance mechanism, via the ``set_user_data`` action. This lets members define initial metadata for certain users; for example to grant specific privileges, associate a human-readable name, or categorise the users. This user-data can then be read (but not written) by user-facing endpoints.
76+
77+
For example, the ``/log/private/admin_only`` endpoint in the C++ logging sample app uses user-data to restrict who is permitted to call it:
78+
79+
.. literalinclude:: ../../src/apps/logging/logging.cpp
80+
:language: cpp
81+
:start-after: SNIPPET_START: user_data_check
82+
:end-before: SNIPPET_END: user_data_check
83+
:dedent: 12
84+
85+
Members configure this permission with ``set_user_data`` proposals:
86+
87+
.. code-block:: bash
88+
89+
$ cat set_user_data_proposal.json
90+
{
91+
"script": {
92+
"text": "tables, args = ...; return Calls:call(\"set_user_data\", args)"
93+
},
94+
"parameter": {
95+
"user_id": 0,
96+
"user_data": {
97+
"isAdmin": true
98+
}
99+
}
100+
}
101+
102+
Once this proposal is accepted, user 0 is able to use this endpoint:
103+
104+
.. code-block:: bash
105+
106+
$ curl https://<ccf-node-address>/app/log/private/admin_only --key user0_privk.pem --cert user0_cert.pem --cacert networkcert.pem -X POST --data-binary '{"id": 42, "msg": "hello world"}' -H "Content-type: application/json" -i
107+
HTTP/1.1 200 OK
108+
109+
true
110+
111+
All other users have empty or non-matching user-data, so will receive a HTTP error if they attempt to access it:
112+
113+
.. code-block:: bash
114+
115+
$ curl https://<ccf-node-address>/app/log/private/admin_only --key user1_privk.pem --cert user1_cert.pem --cacert networkcert.pem -X POST --data-binary '{"id": 42, "msg": "hello world"}' -H "Content-type: application/json" -i
116+
HTTP/1.1 403 Forbidden
117+
118+
Only admins may access this endpoint
73119
74120
Registering the Lua Application
75121
-------------------------------

doc/members/proposals.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Proposing and Voting for a Proposal
33

44
This page explains how members can submit and vote for proposals.
55

6-
Proposals and vote ballots are submitted as Lua scripts. These scripts are executed transactionally, able to read from the current KV state but not write directly to it. Proposals return a list of proposed actions which can make writes, but are only applied when the proposal is accepted. Each vote script is given this list of proposed calls, and also able to read from the KV, and returns a boolean indicating whether it supports or rejects the proposed actions.
6+
Proposals and vote ballots are submitted as Lua scripts. These scripts are executed transactionally, able to read from the current KV state but not write directly to it. Proposals return a list of proposed actions which can make writes, but are only applied when the proposal is accepted. Each vote script is given this list of proposed actions, and also able to read from the KV, and returns a boolean indicating whether it supports or rejects the proposed actions.
77

88
Any member can submit a new proposal. All members can then vote on this proposal using its unique proposal id. Each member may alter their vote (by submitting a new vote) any number of times while the proposal is open. The member who originally submitted the proposal (the `proposer`) votes for the proposal by default, but has the option to include a negative or conditional vote like any other member. Additionally, the proposer has the ability to `withdraw` a proposal while it is open.
99

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"properties": {
4+
"id": {
5+
"maximum": 18446744073709551615,
6+
"minimum": 0,
7+
"type": "integer"
8+
},
9+
"msg": {
10+
"type": "string"
11+
}
12+
},
13+
"required": [
14+
"id",
15+
"msg"
16+
],
17+
"title": "log/private/admin_only/params",
18+
"type": "object"
19+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "log/private/admin_only/result",
4+
"type": "boolean"
5+
}

src/apps/logging/logging.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,49 @@ namespace loggingapp
395395
get_historical, context.get_historical_state(), is_tx_committed))
396396
.install();
397397

398+
auto record_admin_only =
399+
[this, &nwt](ccf::EndpointContext& ctx, nlohmann::json&& params) {
400+
{
401+
// SNIPPET_START: user_data_check
402+
// Check caller's user-data for required permissions
403+
auto users_view = ctx.tx.get_view(nwt.users);
404+
const auto user_opt = users_view->get(ctx.caller_id);
405+
const nlohmann::json user_data = user_opt.has_value() ?
406+
user_opt->user_data :
407+
nlohmann::json(nullptr);
408+
const auto is_admin_it = user_data.find("isAdmin");
409+
410+
// Exit if this user has no user data, or the user data is not an
411+
// object with isAdmin field, or the value of this field is not true
412+
if (
413+
!user_data.is_object() || is_admin_it == user_data.end() ||
414+
!is_admin_it.value().get<bool>())
415+
{
416+
return ccf::make_error(
417+
HTTP_STATUS_FORBIDDEN, "Only admins may access this endpoint");
418+
}
419+
// SNIPPET_END: user_data_check
420+
}
421+
422+
const auto in = params.get<LoggingRecord::In>();
423+
424+
if (in.msg.empty())
425+
{
426+
return ccf::make_error(
427+
HTTP_STATUS_BAD_REQUEST, "Cannot record an empty log message");
428+
}
429+
430+
auto view = ctx.tx.get_view(records);
431+
view->put(in.id, in.msg);
432+
return ccf::make_success(true);
433+
};
434+
make_endpoint(
435+
"log/private/admin_only",
436+
HTTP_POST,
437+
ccf::json_adapter(record_admin_only))
438+
.set_auto_schema<LoggingRecord::In, bool>()
439+
.install();
440+
398441
auto& notifier = context.get_notifier();
399442
nwt.signatures.set_global_hook(
400443
[&notifier](kv::Version version, const ccf::Signatures::Write& w) {

tests/e2e_logging.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,50 @@ def test_update_lua(network, args):
388388
return network
389389

390390

391+
@reqs.description("Test user-data used for access permissions")
392+
@reqs.supports_methods("log/private/admin_only")
393+
def test_user_data_ACL(network, args):
394+
if args.package == "liblogging":
395+
primary, _ = network.find_primary()
396+
397+
proposing_member = network.consortium.get_any_active_member()
398+
user_id = 0
399+
400+
# Give isAdmin permissions to a single user
401+
proposal_body, careful_vote = ccf.proposal_generator.set_user_data(
402+
user_id, {"isAdmin": True},
403+
)
404+
proposal = proposing_member.propose(primary, proposal_body)
405+
proposal.vote_for = careful_vote
406+
network.consortium.vote_using_majority(primary, proposal)
407+
408+
# Confirm that user can now use this endpoint
409+
with primary.client(f"user{user_id}") as c:
410+
r = c.post("/app/log/private/admin_only", {"id": 42, "msg": "hello world"})
411+
assert r.status_code == http.HTTPStatus.OK.value, r.status_code
412+
413+
# Remove permission
414+
proposal_body, careful_vote = ccf.proposal_generator.set_user_data(
415+
user_id, {"isAdmin": False},
416+
)
417+
proposal = proposing_member.propose(primary, proposal_body)
418+
proposal.vote_for = careful_vote
419+
network.consortium.vote_using_majority(primary, proposal)
420+
421+
# Confirm that user is now forbidden on this endpoint
422+
with primary.client(f"user{user_id}") as c:
423+
r = c.post("/app/log/private/admin_only", {"id": 42, "msg": "hello world"})
424+
assert r.status_code == http.HTTPStatus.FORBIDDEN.value, r.status_code
425+
426+
else:
427+
LOG.warning(
428+
f"Skipping {inspect.currentframe().f_code.co_name} as application is not C++"
429+
)
430+
431+
return network
432+
433+
391434
@reqs.description("Check for commit of every prior transaction")
392-
@reqs.supports_methods("/node/commit")
393435
def test_view_history(network, args):
394436
if args.consensus == "pbft":
395437
# This appears to work in PBFT, but it is unacceptably slow:
@@ -586,6 +628,7 @@ def run(args):
586628
network = test_remove(network, args)
587629
network = test_forwarding_frontends(network, args)
588630
network = test_update_lua(network, args)
631+
network = test_user_data_ACL(network, args)
589632
network = test_cert_prefix(network, args)
590633
network = test_anonymous_caller(network, args)
591634
network = test_raw_text(network, args)

tests/suite/test_suite.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
e2e_logging.test_raw_text,
6363
e2e_logging.test_forwarding_frontends,
6464
e2e_logging.test_update_lua,
65+
e2e_logging.test_user_data_ACL,
6566
e2e_logging.test_view_history,
6667
e2e_logging.test_tx_statuses,
6768
# memberclient:

tests/ws_scaffold.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
@reqs.description("Running transactions against logging app")
15-
@reqs.supports_methods("/app/log/private")
15+
@reqs.supports_methods("log/private")
1616
@reqs.at_least_n_nodes(2)
1717
def test(network, args, notifications_queue=None):
1818
primary, other = network.find_primary_and_any_backup()

0 commit comments

Comments
 (0)