CCF
Loading...
Searching...
No Matches
member_frontend.h
Go to the documentation of this file.
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the Apache 2.0 License.
3#pragma once
6#include "ccf/crypto/base64.h"
8#include "ccf/crypto/sha256.h"
9#include "ccf/ds/nonstd.h"
10#include "ccf/http_query.h"
12#include "ccf/json_handler.h"
13#include "ccf/node/quote.h"
18#include "frontend.h"
22#include "node/rpc/call_types.h"
27#include "node/share_manager.h"
28#include "node_interface.h"
32
33#include <charconv>
34#include <exception>
35#include <initializer_list>
36#include <map>
37#include <memory>
38#include <openssl/crypto.h>
39#include <set>
40#include <sstream>
41
42namespace ccf
43{
44 struct SetModule
45 {
46 std::string name;
47 Module module;
48 };
51
53 std::map<std::string, ccf::endpoints::EndpointProperties>;
54
56 {
57 std::map<std::string, JsBundleEndpoint> endpoints;
58 };
61
62 struct JsBundle
63 {
65 std::vector<SetModule> modules;
66 };
68 DECLARE_JSON_REQUIRED_FIELDS(JsBundle, metadata, modules)
69
77
79 {
81 std::optional<ccf::crypto::Pem> public_encryption_key;
82 };
85 FullMemberDetails, status, member_data, cert, public_encryption_key);
86
93
95 {
96 private:
97 // Wrapper for reporting errors, which both logs them under the [gov] tag
98 // and sets the HTTP response
99 static void set_gov_error(
100 const std::shared_ptr<ccf::RpcContext>& rpc_ctx,
101 http_status status,
102 const std::string& code,
103 std::string&& msg)
104 {
106 "{} {} returning error {}: {}",
107 rpc_ctx->get_request_verb().c_str(),
108 rpc_ctx->get_request_path(),
109 status,
110 msg);
111
112 rpc_ctx->set_error(status, code, std::move(msg));
113 }
114
115 void remove_all_other_non_open_proposals(
116 ccf::kv::Tx& tx, const ProposalId& proposal_id)
117 {
118 auto p = tx.rw<ccf::jsgov::ProposalMap>(jsgov::Tables::PROPOSALS);
119 auto pi =
120 tx.rw<ccf::jsgov::ProposalInfoMap>(jsgov::Tables::PROPOSALS_INFO);
121 std::vector<ProposalId> to_be_removed;
122 pi->foreach(
123 [&to_be_removed, &proposal_id](
124 const ProposalId& pid, const ccf::jsgov::ProposalInfo& pinfo) {
125 if (pid != proposal_id && pinfo.state != ProposalState::OPEN)
126 {
127 to_be_removed.push_back(pid);
128 }
129 return true;
130 });
131 for (const auto& pr : to_be_removed)
132 {
133 p->remove(pr);
134 pi->remove(pr);
135 }
136 }
137
138 ccf::jsgov::ProposalInfoSummary resolve_proposal(
139 ccf::kv::Tx& tx,
140 const ProposalId& proposal_id,
141 const std::span<const uint8_t>& proposal_bytes,
142 const std::string& constitution)
143 {
144 auto pi =
145 tx.rw<ccf::jsgov::ProposalInfoMap>(jsgov::Tables::PROPOSALS_INFO);
146 auto pi_ = pi->get(proposal_id);
147
148 const std::string_view proposal{
149 (const char*)proposal_bytes.data(), proposal_bytes.size()};
150
151 std::vector<std::pair<MemberId, bool>> votes;
152 std::optional<ccf::jsgov::Votes> final_votes = std::nullopt;
153 std::optional<ccf::jsgov::VoteFailures> vote_failures = std::nullopt;
154 for (const auto& [mid, mb] : pi_->ballots)
155 {
157
158 auto ballot_func = context.get_exported_function(
159 mb,
160 "vote",
161 fmt::format(
162 "{}[{}].ballots[{}]",
163 ccf::jsgov::Tables::PROPOSALS_INFO,
164 proposal_id,
165 mid));
166
167 std::vector<js::core::JSWrappedValue> argv = {
168 context.new_string(proposal),
169 context.new_string(pi_->proposer_id.value()),
170 // Also pass the proposal_id as a string. This is useful for proposals
171 // that want to refer to themselves in the resolve function, for
172 // example to examine/distinguish themselves other pending proposals.
173 context.new_string(proposal_id)};
174
175 auto val = context.call_with_rt_options(
176 ballot_func,
177 argv,
178 tx.ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
180
181 if (!val.is_exception())
182 {
183 votes.emplace_back(mid, val.is_true());
184 }
185 else
186 {
187 if (!vote_failures.has_value())
188 {
189 vote_failures = ccf::jsgov::VoteFailures();
190 }
191
192 auto [reason, trace] = context.error_message();
193
194 if (context.interrupt_data.request_timed_out)
195 {
196 reason = "Operation took too long to complete.";
197 }
198 vote_failures.value()[mid] = ccf::jsgov::Failure{reason, trace};
199 }
200 }
201
202 {
204
205 auto resolve_func = js_context.get_exported_function(
206 constitution,
207 "resolve",
208 fmt::format("{}[0]", ccf::Tables::CONSTITUTION));
209
210 std::vector<js::core::JSWrappedValue> argv;
211 argv.push_back(js_context.new_string(proposal));
212
213 argv.push_back(js_context.new_string(pi_->proposer_id.value()));
214
215 auto vs = js_context.new_array();
216 size_t index = 0;
217 for (auto& [mid, vote] : votes)
218 {
219 auto v = JS_NewObject(js_context);
220 auto member_id = JS_NewStringLen(js_context, mid.data(), mid.size());
221 JS_DefinePropertyValueStr(
222 js_context, v, "member_id", member_id, JS_PROP_C_W_E);
223 auto vote_status = JS_NewBool(js_context, vote);
224 JS_DefinePropertyValueStr(
225 js_context, v, "vote", vote_status, JS_PROP_C_W_E);
226 JS_DefinePropertyValueUint32(
227 js_context, vs.val, index++, v, JS_PROP_C_W_E);
228 }
229 argv.push_back(vs);
230
231 auto val = js_context.call_with_rt_options(
232 resolve_func,
233 argv,
234 tx.ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
236
237 std::optional<jsgov::Failure> failure = std::nullopt;
238 if (val.is_exception())
239 {
240 pi_.value().state = ProposalState::FAILED;
241 auto [reason, trace] = js_context.error_message();
242 if (js_context.interrupt_data.request_timed_out)
243 {
244 reason = "Operation took too long to complete.";
245 }
246 failure = ccf::jsgov::Failure{
247 fmt::format("Failed to resolve(): {}", reason), trace};
248 }
249 else if (val.is_str())
250 {
251 auto status = js_context.to_str(val).value_or("");
252 if (status == "Open")
253 {
254 pi_.value().state = ProposalState::OPEN;
255 }
256 else if (status == "Accepted")
257 {
258 pi_.value().state = ProposalState::ACCEPTED;
259 }
260 else if (status == "Withdrawn")
261 {
262 pi_.value().state = ProposalState::FAILED;
263 }
264 else if (status == "Rejected")
265 {
266 pi_.value().state = ProposalState::REJECTED;
267 }
268 else if (status == "Failed")
269 {
270 pi_.value().state = ProposalState::FAILED;
271 }
272 else if (status == "Dropped")
273 {
274 pi_.value().state = ProposalState::DROPPED;
275 }
276 else
277 {
278 pi_.value().state = ProposalState::FAILED;
279 failure = ccf::jsgov::Failure{
280 fmt::format(
281 "resolve() returned invalid status value: \"{}\"", status),
282 std::nullopt};
283 }
284 }
285 else
286 {
287 pi_.value().state = ProposalState::FAILED;
288 failure = ccf::jsgov::Failure{
289 "resolve() returned invalid status value", std::nullopt};
290 }
291
292 if (pi_.value().state != ProposalState::OPEN)
293 {
294 remove_all_other_non_open_proposals(tx, proposal_id);
295 final_votes = std::unordered_map<ccf::MemberId, bool>();
296 for (auto& [mid, vote] : votes)
297 {
298 final_votes.value()[mid] = vote;
299 }
300 if (pi_.value().state == ProposalState::ACCEPTED)
301 {
302 auto gov_effects =
304 if (gov_effects == nullptr)
305 {
306 throw std::logic_error(
307 "Unexpected: Could not access GovEffects subsytem");
308 }
309
310 js::CommonContextWithLocalTx apply_js_context(
312
313 apply_js_context.add_extension(
314 std::make_shared<ccf::js::extensions::NodeExtension>(
315 gov_effects.get(), &tx));
316 apply_js_context.add_extension(
317 std::make_shared<ccf::js::extensions::NetworkExtension>(
318 &network, &tx));
319 apply_js_context.add_extension(
320 std::make_shared<ccf::js::extensions::GovEffectsExtension>(&tx));
321
322 auto apply_func = apply_js_context.get_exported_function(
323 constitution,
324 "apply",
325 fmt::format("{}[0]", ccf::Tables::CONSTITUTION));
326
327 std::vector<js::core::JSWrappedValue> apply_argv = {
328 apply_js_context.new_string(proposal),
329 apply_js_context.new_string(proposal_id)};
330
331 auto apply_val = apply_js_context.call_with_rt_options(
332 apply_func,
333 apply_argv,
334 tx.ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
336
337 if (apply_val.is_exception())
338 {
339 pi_.value().state = ProposalState::FAILED;
340 auto [reason, trace] = apply_js_context.error_message();
341 if (apply_js_context.interrupt_data.request_timed_out)
342 {
343 reason = "Operation took too long to complete.";
344 }
345 failure = ccf::jsgov::Failure{
346 fmt::format("Failed to apply(): {}", reason), trace};
347 }
348 }
349 }
350
352 proposal_id,
353 pi_->proposer_id,
354 pi_.value().state,
355 pi_.value().ballots.size(),
356 final_votes,
357 vote_failures,
358 failure};
359 }
360 }
361
362 bool check_member_active(ccf::kv::ReadOnlyTx& tx, const MemberId& id)
363 {
364 return check_member_status(tx, id, {MemberStatus::ACTIVE});
365 }
366
367 bool check_member_status(
369 const MemberId& id,
370 std::initializer_list<MemberStatus> allowed)
371 {
372 auto member = tx.ro(this->network.member_info)->get(id);
373 if (!member.has_value())
374 {
375 return false;
376 }
377 for (const auto s : allowed)
378 {
379 if (member->status == s)
380 {
381 return true;
382 }
383 }
384 return false;
385 }
386
387 void record_voting_history(
388 ccf::kv::Tx& tx,
389 const MemberId& caller_id,
390 const SignedReq& signed_request)
391 {
392 auto governance_history = tx.rw(network.governance_history);
393 governance_history->put(caller_id, {signed_request});
394 }
395
396 void record_cose_governance_history(
397 ccf::kv::Tx& tx,
398 const MemberId& caller_id,
399 const std::span<const uint8_t>& cose_sign1)
400 {
401 auto cose_governance_history = tx.rw(network.cose_governance_history);
402 cose_governance_history->put(
403 caller_id, {cose_sign1.begin(), cose_sign1.end()});
404 }
405
406 ProposalSubmissionStatus is_proposal_submission_acceptable(
407 ccf::kv::Tx& tx,
408 const std::string& created_at,
409 const std::vector<uint8_t>& request_digest,
410 const ccf::ProposalId& proposal_id,
411 ccf::ProposalId& colliding_proposal_id,
412 std::string& min_created_at)
413 {
414 auto cose_recent_proposals = tx.rw(network.cose_recent_proposals);
415 auto key = fmt::format("{}:{}", created_at, ds::to_hex(request_digest));
416
417 std::vector<std::string> replay_keys;
418 cose_recent_proposals->foreach_key(
419 [&replay_keys](const std::string& replay_key) {
420 replay_keys.push_back(replay_key);
421 return true;
422 });
423
424 std::sort(replay_keys.begin(), replay_keys.end());
425
426 // New proposal must be more recent than median proposal kept
427 if (!replay_keys.empty())
428 {
429 min_created_at = std::get<0>(
430 ccf::nonstd::split_1(replay_keys[replay_keys.size() / 2], ":"));
431 auto [key_ts, __] = ccf::nonstd::split_1(key, ":");
432 if (key_ts < min_created_at)
433 {
435 }
436 }
437
438 if (cose_recent_proposals->has(key))
439 {
440 colliding_proposal_id = cose_recent_proposals->get(key).value();
442 }
443 else
444 {
445 size_t window_size = ccf::default_recent_cose_proposals_window_size;
446 auto service = tx.ro(network.config);
447 auto service_config = service->get();
448 if (
449 service_config.has_value() &&
450 service_config->recent_cose_proposals_window_size.has_value())
451 {
452 window_size =
453 service_config->recent_cose_proposals_window_size.value();
454 }
455 cose_recent_proposals->put(key, proposal_id);
456 // Only keep the most recent window_size proposals, to avoid
457 // unbounded memory usage
458 if (replay_keys.size() >= (window_size - 1) /* We just added one */)
459 {
460 for (size_t i = 0; i < (replay_keys.size() - (window_size - 1)); i++)
461 {
462 cose_recent_proposals->remove(replay_keys[i]);
463 }
464 }
466 }
467 }
468
469 bool get_proposal_id_from_path(
470 const ccf::PathParams& params,
471 ProposalId& proposal_id,
472 std::string& error)
473 {
474 return get_path_param(params, "proposal_id", proposal_id, error);
475 }
476
477 bool get_member_id_from_path(
478 const ccf::PathParams& params, MemberId& member_id, std::string& error)
479 {
480 return get_path_param(params, "member_id", member_id.value(), error);
481 }
482
483 template <typename T>
484 void add_kv_wrapper_endpoint(T table)
485 {
486 constexpr bool is_map =
488 constexpr bool is_value =
490
491 if constexpr (!(is_map || is_value))
492 {
493 static_assert(
494 ccf::nonstd::dependent_false_v<T>, "Unsupported table type");
495 }
496
497 auto getter =
498 [&, table](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) {
499 GOV_TRACE_FMT("Called getter for {}", table.get_name());
500 auto response_body = nlohmann::json::object();
501
502 auto handle = ctx.tx.template ro(table);
503 if constexpr (is_map)
504 {
505 handle->foreach([&response_body](const auto& k, const auto& v) {
506 if constexpr (
507 std::is_same_v<typename T::Key, ccf::crypto::Sha256Hash> ||
509 {
510 response_body[k.hex_str()] = v;
511 }
512 else if constexpr (std::is_same_v<
513 typename T::Key,
515 {
516 response_body[k.to_str()] = v;
517 }
518 else
519 {
520 response_body[k] = v;
521 }
522 return true;
523 });
524 }
525 else if constexpr (is_value)
526 {
527 response_body = handle->get();
528 }
529
530 return ccf::make_success(response_body);
531 };
532
533 std::string uri = table.get_name();
534 constexpr auto gov_prefix = "public:ccf.gov.";
535 if (uri.starts_with(gov_prefix))
536 {
537 uri.erase(0, strlen(gov_prefix));
538 }
539 else
540 {
541 throw std::logic_error(fmt::format(
542 "Should only be used to wrap governance tables. '{}' is not "
543 "supported",
544 uri));
545 }
546
547 // Replace . separators with /
548 {
549 auto idx = uri.find('.');
550 while (idx != std::string::npos)
551 {
552 uri[idx] = '/';
553 idx = uri.find('.', idx);
554 }
555 }
556
557 auto endpoint = make_read_only_endpoint(
558 fmt::format("/kv/{}", uri),
559 HTTP_GET,
561 ccf::no_auth_required);
562
563 if constexpr (is_map)
564 {
565 endpoint.template set_auto_schema<
566 void,
567 std::map<typename T::Key, typename T::Value>>();
568 }
569 else if constexpr (is_value)
570 {
571 endpoint.template set_auto_schema<void, typename T::Value>();
572 }
573
574 endpoint.set_openapi_summary(
575 "This route is auto-generated from the KV schema.");
576 endpoint.set_openapi_deprecated(true);
577
578 endpoint.install();
579 }
580
581 void add_kv_wrapper_endpoints()
582 {
583 const auto all_gov_tables = network.get_all_builtin_governance_tables();
584 ccf::nonstd::tuple_for_each(
585 all_gov_tables, [this](auto table) { add_kv_wrapper_endpoint(table); });
586 }
587
588 NetworkState& network;
589 ShareManager share_manager;
590
591 public:
593 NetworkState& network_, ccf::AbstractNodeContext& context_) :
594 GovEndpointRegistry(network_, context_),
595 network(network_),
596 share_manager(network_.ledger_secrets)
597 {
598 openapi_info.title = "CCF Governance API";
600 "This API is used to submit and query proposals which affect CCF's "
601 "public governance tables.";
603 }
604
605 static std::optional<MemberId> get_caller_member_id(
607 {
608 if (
609 const auto* cose_ident =
611 {
612 return cose_ident->member_id;
613 }
614 else if (
615 const auto* cert_ident =
617 {
618 return cert_ident->member_id;
619 }
620
621 GOV_FAIL_FMT("Request was not authenticated with a member auth policy");
622 return std::nullopt;
623 }
624
627 std::optional<MemberId>& member_id,
628 std::optional<ccf::MemberCOSESign1AuthnIdentity>& cose_auth_id,
629 bool must_be_active = true)
630 {
631 if (
632 const auto* cose_ident =
634 {
635 member_id = cose_ident->member_id;
636 cose_auth_id = *cose_ident;
637 }
638 else
639 {
640 set_gov_error(
641 ctx.rpc_ctx,
642 HTTP_STATUS_FORBIDDEN,
643 ccf::errors::AuthorizationFailed,
644 "Caller is a not a valid member id");
645
646 return false;
647 }
648
649 if (must_be_active && !check_member_active(ctx.tx, member_id.value()))
650 {
651 set_gov_error(
652 ctx.rpc_ctx,
653 HTTP_STATUS_FORBIDDEN,
654 ccf::errors::AuthorizationFailed,
655 fmt::format("Member {} is not active.", member_id.value()));
656 return false;
657 }
658
659 return true;
660 }
661
662 AuthnPolicies member_sig_only_policies(const std::string& gov_msg_type)
663 {
664 return {std::make_shared<MemberCOSESign1AuthnPolicy>(gov_msg_type)};
665 }
666
667 AuthnPolicies member_cert_or_sig_policies(const std::string& gov_msg_type)
668 {
669 return {
670 member_cert_auth_policy,
671 std::make_shared<MemberCOSESign1AuthnPolicy>(gov_msg_type)};
672 }
673
674 void init_handlers() override
675 {
677
679 auto ack = [this](ccf::endpoints::EndpointContext& ctx) {
680 std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
681 std::nullopt;
682 std::optional<MemberId> member_id = std::nullopt;
683 if (!authnz_active_member(ctx, member_id, cose_auth_id, false))
684 {
685 return;
686 }
687
688 auto params = nlohmann::json::parse(
689 cose_auth_id.has_value() ? cose_auth_id->content :
690 ctx.rpc_ctx->get_request_body());
691
692 auto mas = ctx.tx.rw(this->network.member_acks);
693 const auto ma = mas->get(member_id.value());
694 if (!ma)
695 {
696 set_gov_error(
697 ctx.rpc_ctx,
698 HTTP_STATUS_FORBIDDEN,
699 ccf::errors::AuthorizationFailed,
700 fmt::format(
701 "No ACK record exists for caller {}.", member_id.value()));
702 return;
703 }
704
705 const auto digest = params.get<StateDigest>();
706 if (ma->state_digest != digest.state_digest)
707 {
708 set_gov_error(
709 ctx.rpc_ctx,
710 HTTP_STATUS_BAD_REQUEST,
711 ccf::errors::StateDigestMismatch,
712 "Submitted state digest is not valid.");
713 return;
714 }
715
716 auto sig = ctx.tx.rw(this->network.signatures);
717 const auto s = sig->get();
718 if (cose_auth_id.has_value())
719 {
720 std::vector<uint8_t> cose_sign1 = {
721 cose_auth_id->envelope.begin(), cose_auth_id->envelope.end()};
722 if (!s)
723 {
724 mas->put(member_id.value(), MemberAck({}, cose_sign1));
725 }
726 else
727 {
728 mas->put(member_id.value(), MemberAck(s->root, cose_sign1));
729 }
730 }
731
732 // update member status to ACTIVE
733 try
734 {
735 InternalTablesAccess::activate_member(ctx.tx, member_id.value());
736 }
737 catch (const std::logic_error& e)
738 {
739 set_gov_error(
740 ctx.rpc_ctx,
741 HTTP_STATUS_FORBIDDEN,
742 ccf::errors::AuthorizationFailed,
743 fmt::format("Error activating new member: {}", e.what()));
744 return;
745 }
746
747 auto service_status = InternalTablesAccess::get_service_status(ctx.tx);
748 if (!service_status.has_value())
749 {
750 set_gov_error(
751 ctx.rpc_ctx,
752 HTTP_STATUS_INTERNAL_SERVER_ERROR,
753 ccf::errors::InternalError,
754 "No service currently available.");
755 return;
756 }
757
758 auto members = ctx.tx.rw(this->network.member_info);
759 auto member_info = members->get(member_id.value());
760 if (
761 service_status.value() == ServiceStatus::OPEN &&
762 InternalTablesAccess::is_recovery_member(ctx.tx, member_id.value()))
763 {
764 // When the service is OPEN and the new active member is a recovery
765 // member, all recovery members are allocated new recovery shares
766 try
767 {
768 share_manager.shuffle_recovery_shares(ctx.tx);
769 }
770 catch (const std::logic_error& e)
771 {
772 set_gov_error(
773 ctx.rpc_ctx,
774 HTTP_STATUS_INTERNAL_SERVER_ERROR,
775 ccf::errors::InternalError,
776 fmt::format("Error issuing new recovery shares: {}", e.what()));
777 return;
778 }
779 }
780 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
781 return;
782 };
783 make_endpoint("/ack", HTTP_POST, ack, member_sig_only_policies("ack"))
785 "Provide a member endorsement of a service state digest")
787 .set_openapi_deprecated_replaced(
788 "5.0.0", "POST /gov/members/state-digests/{memberId}:ack")
789 .install();
790
792 auto update_state_digest = [this](ccf::endpoints::EndpointContext& ctx) {
793 const auto member_id = get_caller_member_id(ctx);
794 if (!member_id.has_value())
795 {
796 set_gov_error(
797 ctx.rpc_ctx,
798 HTTP_STATUS_FORBIDDEN,
799 ccf::errors::AuthorizationFailed,
800 "Caller is a not a valid member id");
801 return;
802 }
803
804 auto mas = ctx.tx.rw(this->network.member_acks);
805 auto sig = ctx.tx.rw(this->network.signatures);
806 auto ma = mas->get(member_id.value());
807 if (!ma)
808 {
809 set_gov_error(
810 ctx.rpc_ctx,
811 HTTP_STATUS_FORBIDDEN,
812 ccf::errors::AuthorizationFailed,
813 fmt::format(
814 "No ACK record exists for caller {}.", member_id.value()));
815 return;
816 }
817
818 auto s = sig->get();
819 if (s)
820 {
821 ma->state_digest = s->root.hex_str();
822 mas->put(member_id.value(), ma.value());
823 }
824 nlohmann::json j;
825 j["state_digest"] = ma->state_digest;
826
827 ctx.rpc_ctx->set_response_header(
828 ccf::http::headers::CONTENT_TYPE,
829 http::headervalues::contenttype::JSON);
830 ctx.rpc_ctx->set_response_body(j.dump());
831 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
832 return;
833 };
835 "/ack/update_state_digest",
836 HTTP_POST,
837 update_state_digest,
838 member_cert_or_sig_policies("state_digest"))
840 .set_openapi_summary(
841 "Update and fetch a service state digest, for the purpose of member "
842 "endorsement")
843 .set_openapi_deprecated_replaced(
844 "5.0.0", "POST /gov/members/state-digests/{memberId}:update")
845 .install();
846
847 auto get_encrypted_recovery_share =
849 const auto member_id = get_caller_member_id(ctx);
850 if (!member_id.has_value())
851 {
852 set_gov_error(
853 ctx.rpc_ctx,
854 HTTP_STATUS_FORBIDDEN,
855 ccf::errors::AuthorizationFailed,
856 "Member is unknown.");
857 return;
858 }
859 if (!check_member_active(ctx.tx, member_id.value()))
860 {
861 set_gov_error(
862 ctx.rpc_ctx,
863 HTTP_STATUS_FORBIDDEN,
864 ccf::errors::AuthorizationFailed,
865 "Only active members are given recovery shares.");
866 return;
867 }
868
869 auto encrypted_share =
870 share_manager.get_encrypted_share(ctx.tx, member_id.value());
871
872 if (!encrypted_share.has_value())
873 {
874 set_gov_error(
875 ctx.rpc_ctx,
876 HTTP_STATUS_NOT_FOUND,
877 ccf::errors::ResourceNotFound,
878 fmt::format(
879 "Recovery share not found for member {}.", member_id->value()));
880 return;
881 }
882
883 auto rec_share = GetRecoveryShare::Out{
884 ccf::crypto::b64_from_raw(encrypted_share.value())};
885 ctx.rpc_ctx->set_response_header(
886 ccf::http::headers::CONTENT_TYPE,
887 http::headervalues::contenttype::JSON);
888 ctx.rpc_ctx->set_response_body(nlohmann::json(rec_share).dump());
889 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
890 return;
891 };
893 "/recovery_share",
894 HTTP_GET,
895 get_encrypted_recovery_share,
896 member_cert_or_sig_policies("encrypted_recovery_share"))
898 .set_openapi_summary("A member's recovery share")
899 .set_openapi_deprecated_replaced(
900 "5.0.0", "GET /gov/recovery/encrypted-shares/{memberId}")
901 .install();
902
903 auto get_encrypted_recovery_share_for_member =
905 std::string error_msg;
906 MemberId member_id;
907 if (!get_member_id_from_path(
908 ctx.rpc_ctx->get_request_path_params(), member_id, error_msg))
909 {
910 set_gov_error(
911 ctx.rpc_ctx,
912 HTTP_STATUS_BAD_REQUEST,
913 ccf::errors::InvalidResourceName,
914 std::move(error_msg));
915 return;
916 }
917
918 auto encrypted_share =
919 share_manager.get_encrypted_share(ctx.tx, member_id);
920
921 if (!encrypted_share.has_value())
922 {
923 set_gov_error(
924 ctx.rpc_ctx,
925 HTTP_STATUS_NOT_FOUND,
926 ccf::errors::ResourceNotFound,
927 fmt::format(
928 "Recovery share not found for member {}.", member_id));
929 return;
930 }
931
932 auto rec_share = GetRecoveryShare::Out{
933 ccf::crypto::b64_from_raw(encrypted_share.value())};
934 ctx.rpc_ctx->set_response_header(
935 ccf::http::headers::CONTENT_TYPE,
936 http::headervalues::contenttype::JSON);
937 ctx.rpc_ctx->set_response_body(nlohmann::json(rec_share).dump());
938 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
939 return;
940 };
942 "/encrypted_recovery_share/{member_id}",
943 HTTP_GET,
944 get_encrypted_recovery_share_for_member,
945 ccf::no_auth_required)
947 .set_openapi_summary("A member's recovery share")
948 .set_openapi_deprecated_replaced(
949 "5.0.0", "GET /gov/recovery/encrypted-shares/{memberId}")
950 .install();
951
952 auto submit_recovery_share = [this](
954 // Only active members can submit their shares for recovery
955 const auto member_id = get_caller_member_id(ctx);
956 if (!member_id.has_value())
957 {
958 set_gov_error(
959 ctx.rpc_ctx,
960 HTTP_STATUS_FORBIDDEN,
961 ccf::errors::AuthorizationFailed,
962 "Member is unknown.");
963 return;
964 }
965 if (!check_member_active(ctx.tx, member_id.value()))
966 {
967 set_gov_error(
968 ctx.rpc_ctx,
969 HTTP_STATUS_FORBIDDEN,
970 errors::AuthorizationFailed,
971 "Member is not active.");
972 return;
973 }
974
975 const auto* cose_auth_id =
976 ctx.try_get_caller<ccf::MemberCOSESign1AuthnIdentity>();
977 auto params = nlohmann::json::parse(
978 cose_auth_id ? cose_auth_id->content :
979 ctx.rpc_ctx->get_request_body());
980
981 if (
984 {
985 set_gov_error(
986 ctx.rpc_ctx,
987 HTTP_STATUS_FORBIDDEN,
988 errors::ServiceNotWaitingForRecoveryShares,
989 "Service is not waiting for recovery shares.");
990 return;
991 }
992
993 auto node_operation = context.get_subsystem<AbstractNodeOperation>();
994 if (node_operation == nullptr)
995 {
996 throw std::logic_error(
997 "Unexpected: Could not access NodeOperation subsystem");
998 }
999
1000 if (node_operation->is_reading_private_ledger())
1001 {
1002 set_gov_error(
1003 ctx.rpc_ctx,
1004 HTTP_STATUS_FORBIDDEN,
1005 errors::NodeAlreadyRecovering,
1006 "Node is already recovering private ledger.");
1007 return;
1008 }
1009
1010 std::string share = params["share"];
1011 auto raw_recovery_share = ccf::crypto::raw_from_b64(share);
1012 OPENSSL_cleanse(const_cast<char*>(share.data()), share.size());
1013
1014 size_t submitted_shares_count = 0;
1015 try
1016 {
1017 submitted_shares_count = share_manager.submit_recovery_share(
1018 ctx.tx, member_id.value(), raw_recovery_share);
1019 }
1020 catch (const std::exception& e)
1021 {
1022 constexpr auto error_msg = "Error submitting recovery shares.";
1023 GOV_FAIL_FMT(error_msg);
1024 GOV_DEBUG_FMT("Error: {}", e.what());
1025 set_gov_error(
1026 ctx.rpc_ctx,
1027 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1028 errors::InternalError,
1029 error_msg);
1030 return;
1031 }
1032 OPENSSL_cleanse(raw_recovery_share.data(), raw_recovery_share.size());
1033
1034 if (
1035 submitted_shares_count <
1037 {
1038 // The number of shares required to re-assemble the secret has not yet
1039 // been reached
1040 auto recovery_share = SubmitRecoveryShare::Out{fmt::format(
1041 "{}/{} recovery shares successfully submitted.",
1042 submitted_shares_count,
1044 ctx.rpc_ctx->set_response_header(
1045 ccf::http::headers::CONTENT_TYPE,
1046 http::headervalues::contenttype::JSON);
1047 ctx.rpc_ctx->set_response_body(nlohmann::json(recovery_share).dump());
1048 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1049 return;
1050 }
1051
1053 "Reached recovery threshold {}",
1055
1056 try
1057 {
1058 node_operation->initiate_private_recovery(ctx.tx);
1059 }
1060 catch (const std::exception& e)
1061 {
1062 // Clear the submitted shares if combination fails so that members can
1063 // start over.
1064 constexpr auto error_msg = "Failed to initiate private recovery.";
1065 GOV_FAIL_FMT(error_msg);
1066 GOV_DEBUG_FMT("Error: {}", e.what());
1068 ctx.rpc_ctx->set_apply_writes(true);
1069 set_gov_error(
1070 ctx.rpc_ctx,
1071 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1072 errors::InternalError,
1073 error_msg);
1074 return;
1075 }
1076
1077 auto recovery_share = SubmitRecoveryShare::Out{fmt::format(
1078 "{}/{} recovery shares successfully submitted. End of recovery "
1079 "procedure initiated.",
1080 submitted_shares_count,
1082 ctx.rpc_ctx->set_response_header(
1083 ccf::http::headers::CONTENT_TYPE,
1084 http::headervalues::contenttype::JSON);
1085 ctx.rpc_ctx->set_response_body(nlohmann::json(recovery_share).dump());
1086 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1087 };
1089 "/recovery_share",
1090 HTTP_POST,
1091 submit_recovery_share,
1092 member_cert_or_sig_policies("recovery_share"))
1094 .set_openapi_summary(
1095 "Provide a recovery share for the purpose of completing a service "
1096 "recovery")
1097 .set_openapi_deprecated_replaced(
1098 "5.0.0", "POST /gov/recovery/members/{memberId}:recover")
1099 .install();
1100
1101 using JWTKeyMap = std::map<JwtKeyId, std::vector<KeyIdInfo>>;
1102
1103 auto get_jwt_keys = [this](auto& ctx, nlohmann::json&& body) {
1104 auto keys = ctx.tx.ro(network.jwt_public_signing_keys_metadata);
1105 JWTKeyMap kmap;
1106 keys->foreach([&kmap](const auto& k, const auto& v) {
1107 std::vector<KeyIdInfo> info;
1108 for (const auto& metadata : v)
1109 {
1110 info.push_back(KeyIdInfo{
1111 metadata.issuer, ccf::crypto::cert_der_to_pem(metadata.cert)});
1112 }
1113 kmap.emplace(k, std::move(info));
1114 return true;
1115 });
1116
1117 return make_success(kmap);
1118 };
1120 "/jwt_keys/all", HTTP_GET, json_adapter(get_jwt_keys), no_auth_required)
1121 .set_auto_schema<void, JWTKeyMap>()
1122 .set_openapi_deprecated_replaced("5.0.0", "POST /gov/service/jwk")
1123 .install();
1124
1125 auto post_proposals_js = [this](ccf::endpoints::EndpointContext& ctx) {
1126 std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
1127 std::nullopt;
1128 std::optional<MemberId> member_id = std::nullopt;
1129 if (!authnz_active_member(ctx, member_id, cose_auth_id))
1130 {
1131 return;
1132 }
1133
1134 if (!consensus)
1135 {
1136 set_gov_error(
1137 ctx.rpc_ctx,
1138 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1139 ccf::errors::InternalError,
1140 "No consensus available.");
1141 return;
1142 }
1143
1144 std::vector<uint8_t> request_digest;
1145 if (cose_auth_id.has_value())
1146 {
1147 std::span<const uint8_t> sig = cose_auth_id->signature;
1148 request_digest = ccf::crypto::sha256(sig);
1149 }
1150
1151 ProposalId proposal_id;
1152 auto root_at_read = ctx.tx.get_root_at_read_version();
1153 if (!root_at_read.has_value())
1154 {
1155 set_gov_error(
1156 ctx.rpc_ctx,
1157 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1158 ccf::errors::InternalError,
1159 "Proposal failed to bind to state.");
1160 return;
1161 }
1162
1163 // caller_identity.request_digest is set when getting the
1164 // MemberSignatureAuthnIdentity identity. The proposal id is a
1165 // digest of the root of the state tree at the read version and the
1166 // request digest.
1167 std::vector<uint8_t> acc(
1168 root_at_read.value().h.begin(), root_at_read.value().h.end());
1169 acc.insert(acc.end(), request_digest.begin(), request_digest.end());
1170 const ccf::crypto::Sha256Hash proposal_digest(acc);
1171 proposal_id = proposal_digest.hex_str();
1172
1173 auto constitution = ctx.tx.ro(network.constitution)->get();
1174 if (!constitution.has_value())
1175 {
1176 set_gov_error(
1177 ctx.rpc_ctx,
1178 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1179 ccf::errors::InternalError,
1180 "No constitution is set - proposals cannot be evaluated");
1181 return;
1182 }
1183
1184 auto validate_script = constitution.value();
1185
1187
1188 auto validate_func = context.get_exported_function(
1189 validate_script,
1190 "validate",
1191 fmt::format("{}[0]", ccf::Tables::CONSTITUTION));
1192
1193 const std::span<const uint8_t> proposal_body =
1194 cose_auth_id.has_value() ? cose_auth_id->content :
1195 ctx.rpc_ctx->get_request_body();
1196
1197 auto body = reinterpret_cast<const char*>(proposal_body.data());
1198 auto body_len = proposal_body.size();
1199
1200 auto proposal = context.new_string_len(body, body_len);
1201 auto val = context.call_with_rt_options(
1202 validate_func,
1203 {proposal},
1204 ctx.tx.ro<ccf::JSEngine>(ccf::Tables::JSENGINE)->get(),
1206
1207 if (val.is_exception())
1208 {
1209 auto [reason, trace] = context.error_message();
1210 if (context.interrupt_data.request_timed_out)
1211 {
1212 reason = "Operation took too long to complete.";
1213 }
1214 set_gov_error(
1215 ctx.rpc_ctx,
1216 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1217 ccf::errors::InternalError,
1218 fmt::format(
1219 "Failed to execute validation: {} {}",
1220 reason,
1221 trace.value_or("")));
1222 return;
1223 }
1224
1225 if (!val.is_obj())
1226 {
1227 set_gov_error(
1228 ctx.rpc_ctx,
1229 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1230 ccf::errors::InternalError,
1231 "Validation failed to return an object");
1232 return;
1233 }
1234
1235 std::string description;
1236 auto desc = val["description"];
1237 if (desc.is_str())
1238 {
1239 description = context.to_str(desc).value_or("");
1240 }
1241
1242 auto valid = val["valid"];
1243 if (!valid.is_true())
1244 {
1245 set_gov_error(
1246 ctx.rpc_ctx,
1247 HTTP_STATUS_BAD_REQUEST,
1248 ccf::errors::ProposalFailedToValidate,
1249 fmt::format("Proposal failed to validate: {}", description));
1250 return;
1251 }
1252
1253 auto pm = ctx.tx.rw<ccf::jsgov::ProposalMap>(jsgov::Tables::PROPOSALS);
1254 // Introduce a read dependency, so that if identical proposal
1255 // creations are in-flight and reading at the same version, all except
1256 // the first conflict and are re-executed. If we ever produce a
1257 // proposal ID which already exists, we must have a hash collision.
1258 if (pm->has(proposal_id))
1259 {
1260 set_gov_error(
1261 ctx.rpc_ctx,
1262 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1263 ccf::errors::InternalError,
1264 "Proposal ID collision.");
1265 return;
1266 }
1267 pm->put(proposal_id, {proposal_body.begin(), proposal_body.end()});
1268
1269 auto pi =
1270 ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(jsgov::Tables::PROPOSALS_INFO);
1271 pi->put(proposal_id, {member_id.value(), ccf::ProposalState::OPEN, {}});
1272
1273 if (cose_auth_id.has_value())
1274 {
1275 record_cose_governance_history(
1276 ctx.tx, member_id.value(), cose_auth_id->envelope);
1277 ccf::ProposalId colliding_proposal_id = proposal_id;
1278 std::string min_created_at = "";
1279 // created_at, submitted as a binary integer number of seconds since
1280 // epoch in the COSE Sign1 envelope, is converted to a decimal
1281 // representation in ASCII, stored as a string, and compared
1282 // alphanumerically. This is partly to keep governance as text-based
1283 // as possible, to faciliate audit, but also to be able to benefit
1284 // from future planned ordering support in the KV. To compare
1285 // correctly, the string representation needs to be padded with
1286 // leading zeroes, and must therefore not exceed a fixed digit width.
1287 // 10 digits is enough to last until November 2286, ie. long enough.
1288 if (cose_auth_id->protected_header.gov_msg_created_at > 9'999'999'999)
1289 {
1290 set_gov_error(
1291 ctx.rpc_ctx,
1292 HTTP_STATUS_BAD_REQUEST,
1293 ccf::errors::InvalidCreatedAt,
1294 "Header parameter created_at value is too large");
1295 return;
1296 }
1297 std::string created_at_str = fmt::format(
1298 "{:0>10}", cose_auth_id->protected_header.gov_msg_created_at);
1299 const auto acceptable = is_proposal_submission_acceptable(
1300 ctx.tx,
1301 created_at_str,
1302 request_digest,
1303 proposal_id,
1304 colliding_proposal_id,
1305 min_created_at);
1306 switch (acceptable)
1307 {
1309 {
1310 set_gov_error(
1311 ctx.rpc_ctx,
1312 HTTP_STATUS_BAD_REQUEST,
1313 ccf::errors::ProposalCreatedTooLongAgo,
1314 fmt::format(
1315 "Proposal created too long ago, created_at must be greater "
1316 "than {}",
1317 min_created_at));
1318 return;
1319 }
1321 {
1322 set_gov_error(
1323 ctx.rpc_ctx,
1324 HTTP_STATUS_BAD_REQUEST,
1325 ccf::errors::ProposalReplay,
1326 fmt::format(
1327 "Proposal submission replay, already exists as proposal {}",
1328 colliding_proposal_id));
1329 return;
1330 }
1332 break;
1333 default:
1334 throw std::runtime_error(
1335 "Invalid ProposalSubmissionStatus value");
1336 };
1337 }
1338
1339 auto rv = resolve_proposal(
1340 ctx.tx, proposal_id, proposal_body, constitution.value());
1341
1342 if (rv.state == ProposalState::FAILED)
1343 {
1344 // If the proposal failed to apply, we want to discard the tx and not
1345 // apply its side-effects to the KV state.
1346 set_gov_error(
1347 ctx.rpc_ctx,
1348 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1349 ccf::errors::InternalError,
1350 fmt::format("{}", rv.failure));
1351 return;
1352 }
1353 else
1354 {
1355 pi->put(
1356 proposal_id,
1357 {member_id.value(), rv.state, {}, {}, std::nullopt, rv.failure});
1358 ctx.rpc_ctx->set_response_header(
1359 ccf::http::headers::CONTENT_TYPE,
1360 http::headervalues::contenttype::JSON);
1361 ctx.rpc_ctx->set_response_body(nlohmann::json(rv).dump());
1362 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1363 return;
1364 }
1365 };
1366
1368 "/proposals",
1369 HTTP_POST,
1370 post_proposals_js,
1371 member_sig_only_policies("proposal"))
1373 .set_openapi_deprecated_replaced(
1374 "5.0.0", "POST /gov/members/proposals:create")
1375 .install();
1376
1377 using AllOpenProposals = std::map<ProposalId, jsgov::ProposalInfo>;
1378 auto get_open_proposals_js =
1379 [this](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) {
1380 auto proposal_info = ctx.tx.ro<ccf::jsgov::ProposalInfoMap>(
1381 jsgov::Tables::PROPOSALS_INFO);
1382 AllOpenProposals response;
1383 proposal_info->foreach(
1384 [&response](
1385 const ProposalId& pid, const ccf::jsgov::ProposalInfo& pinfo) {
1386 if (pinfo.state == ProposalState::OPEN)
1387 {
1388 response[pid] = pinfo;
1389 }
1390 return true;
1391 });
1392 return make_success(response);
1393 };
1394
1396 "/proposals",
1397 HTTP_GET,
1398 json_read_only_adapter(get_open_proposals_js),
1399 ccf::no_auth_required)
1400 .set_auto_schema<void, AllOpenProposals>()
1401 .set_openapi_summary(
1402 "Proposed changes to the service pending resolution")
1403 .set_openapi_deprecated_replaced("5.0.0", "GET /gov/members/proposals")
1404 .install();
1405
1406 auto get_proposal_js = [this](
1408 nlohmann::json&&) {
1409 // Take expand=ballots, return eg. "ballots": 3 if not set
1410 // or "ballots": list of ballots in full if passed
1411
1412 ProposalId proposal_id;
1413 std::string error;
1414 if (!get_proposal_id_from_path(
1415 ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
1416 {
1417 return make_error(
1418 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
1419 }
1420
1421 auto pm = ctx.tx.ro<ccf::jsgov::ProposalMap>(jsgov::Tables::PROPOSALS);
1422 auto p = pm->get(proposal_id);
1423
1424 if (!p)
1425 {
1426 return make_error(
1427 HTTP_STATUS_NOT_FOUND,
1428 ccf::errors::ProposalNotFound,
1429 fmt::format("Proposal {} does not exist.", proposal_id));
1430 }
1431
1432 auto pi =
1433 ctx.tx.ro<ccf::jsgov::ProposalInfoMap>(jsgov::Tables::PROPOSALS_INFO);
1434 auto pi_ = pi->get(proposal_id);
1435
1436 if (!pi_)
1437 {
1438 return make_error(
1439 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1440 ccf::errors::InternalError,
1441 fmt::format(
1442 "No proposal info associated with {} exists.", proposal_id));
1443 }
1444
1445 return make_success(pi_.value());
1446 };
1447
1449 "/proposals/{proposal_id}",
1450 HTTP_GET,
1451 json_read_only_adapter(get_proposal_js),
1452 ccf::no_auth_required)
1454 .set_openapi_summary(
1455 "Information about a proposed change to the service")
1456 .set_openapi_deprecated_replaced(
1457 "5.0.0", "GET /gov/members/proposals/{proposalId}")
1458 .install();
1459
1460 auto withdraw_js = [this](ccf::endpoints::EndpointContext& ctx) {
1461 std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
1462 std::nullopt;
1463 std::optional<MemberId> member_id = std::nullopt;
1464 if (!authnz_active_member(ctx, member_id, cose_auth_id))
1465 {
1466 return;
1467 }
1468
1469 ProposalId proposal_id;
1470 std::string error;
1471 if (!get_proposal_id_from_path(
1472 ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
1473 {
1474 set_gov_error(
1475 ctx.rpc_ctx,
1476 HTTP_STATUS_BAD_REQUEST,
1477 ccf::errors::InvalidResourceName,
1478 std::move(error));
1479 return;
1480 }
1481
1482 if (cose_auth_id.has_value())
1483 {
1484 if (!(cose_auth_id->protected_header.gov_msg_proposal_id
1485 .has_value() &&
1486 cose_auth_id->protected_header.gov_msg_proposal_id.value() ==
1487 proposal_id))
1488 {
1489 set_gov_error(
1490 ctx.rpc_ctx,
1491 HTTP_STATUS_BAD_REQUEST,
1492 ccf::errors::InvalidResourceName,
1493 "Authenticated proposal id does not match URL");
1494 return;
1495 }
1496 }
1497
1498 auto pi =
1499 ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(jsgov::Tables::PROPOSALS_INFO);
1500 auto pi_ = pi->get(proposal_id);
1501
1502 if (!pi_)
1503 {
1504 set_gov_error(
1505 ctx.rpc_ctx,
1506 HTTP_STATUS_BAD_REQUEST,
1507 ccf::errors::ProposalNotFound,
1508 fmt::format("Proposal {} does not exist.", proposal_id));
1509 return;
1510 }
1511
1512 if (member_id.value() != pi_->proposer_id)
1513 {
1514 set_gov_error(
1515 ctx.rpc_ctx,
1516 HTTP_STATUS_FORBIDDEN,
1517 ccf::errors::AuthorizationFailed,
1518 fmt::format(
1519 "Proposal {} can only be withdrawn by proposer {}, not caller "
1520 "{}.",
1521 proposal_id,
1522 pi_->proposer_id,
1523 member_id.value()));
1524 return;
1525 }
1526
1527 if (pi_->state != ProposalState::OPEN)
1528 {
1529 set_gov_error(
1530 ctx.rpc_ctx,
1531 HTTP_STATUS_BAD_REQUEST,
1532 ccf::errors::ProposalNotOpen,
1533 fmt::format(
1534 "Proposal {} is currently in state {} - only {} proposals can be "
1535 "withdrawn.",
1536 proposal_id,
1537 pi_->state,
1539 return;
1540 }
1541
1542 pi_->state = ProposalState::WITHDRAWN;
1543 pi->put(proposal_id, pi_.value());
1544
1545 remove_all_other_non_open_proposals(ctx.tx, proposal_id);
1546 if (cose_auth_id.has_value())
1547 {
1548 record_cose_governance_history(
1549 ctx.tx, member_id.value(), cose_auth_id->envelope);
1550 }
1551
1552 ctx.rpc_ctx->set_response_header(
1553 ccf::http::headers::CONTENT_TYPE,
1554 http::headervalues::contenttype::JSON);
1555 ctx.rpc_ctx->set_response_body(nlohmann::json(pi_.value()).dump());
1556 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1557 };
1558
1560 "/proposals/{proposal_id}/withdraw",
1561 HTTP_POST,
1562 withdraw_js,
1563 member_sig_only_policies("withdrawal"))
1565 .set_openapi_summary("Withdraw a proposed change to the service")
1566 .set_openapi_deprecated_replaced(
1567 "5.0.0", "POST /gov/members/proposals/{proposalId}:withdraw")
1568 .install();
1569
1570 auto get_proposal_actions_js =
1572 ProposalId proposal_id;
1573 std::string error;
1574 if (!get_proposal_id_from_path(
1575 ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
1576 {
1577 set_gov_error(
1578 ctx.rpc_ctx,
1579 HTTP_STATUS_BAD_REQUEST,
1580 ccf::errors::InvalidResourceName,
1581 std::move(error));
1582 return;
1583 }
1584
1585 auto pm =
1586 ctx.tx.ro<ccf::jsgov::ProposalMap>(jsgov::Tables::PROPOSALS);
1587 auto p = pm->get(proposal_id);
1588
1589 if (!p)
1590 {
1591 set_gov_error(
1592 ctx.rpc_ctx,
1593 HTTP_STATUS_NOT_FOUND,
1594 ccf::errors::ProposalNotFound,
1595 fmt::format("Proposal {} does not exist.", proposal_id));
1596 return;
1597 }
1598
1599 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1600 ctx.rpc_ctx->set_response_header(
1601 ccf::http::headers::CONTENT_TYPE,
1602 http::headervalues::contenttype::JSON);
1603 ctx.rpc_ctx->set_response_body(std::move(p.value()));
1604 };
1605
1607 "/proposals/{proposal_id}/actions",
1608 HTTP_GET,
1609 get_proposal_actions_js,
1610 ccf::no_auth_required)
1612 .set_openapi_summary(
1613 "Actions contained in a proposed change to the service")
1614 .set_openapi_deprecated_replaced(
1615 "5.0.0", "GET /gov/members/proposals/{proposalId}/actions")
1616 .install();
1617
1618 auto vote_js = [this](ccf::endpoints::EndpointContext& ctx) {
1619 std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
1620 std::nullopt;
1621 std::optional<MemberId> member_id = std::nullopt;
1622 if (!authnz_active_member(ctx, member_id, cose_auth_id))
1623 {
1624 return;
1625 }
1626
1627 ProposalId proposal_id;
1628 std::string error;
1629 if (!get_proposal_id_from_path(
1630 ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
1631 {
1632 set_gov_error(
1633 ctx.rpc_ctx,
1634 HTTP_STATUS_BAD_REQUEST,
1635 ccf::errors::InvalidResourceName,
1636 std::move(error));
1637 return;
1638 }
1639 if (cose_auth_id.has_value())
1640 {
1641 if (!(cose_auth_id->protected_header.gov_msg_proposal_id
1642 .has_value() &&
1643 cose_auth_id->protected_header.gov_msg_proposal_id.value() ==
1644 proposal_id))
1645 {
1646 set_gov_error(
1647 ctx.rpc_ctx,
1648 HTTP_STATUS_BAD_REQUEST,
1649 ccf::errors::InvalidResourceName,
1650 "Authenticated proposal id does not match URL");
1651 return;
1652 }
1653 }
1654
1655 auto constitution = ctx.tx.ro(network.constitution)->get();
1656 if (!constitution.has_value())
1657 {
1658 set_gov_error(
1659 ctx.rpc_ctx,
1660 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1661 ccf::errors::InternalError,
1662 "No constitution is set - proposals cannot be evaluated");
1663 return;
1664 }
1665
1666 auto pi =
1667 ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(jsgov::Tables::PROPOSALS_INFO);
1668 auto pi_ = pi->get(proposal_id);
1669 if (!pi_)
1670 {
1671 set_gov_error(
1672 ctx.rpc_ctx,
1673 HTTP_STATUS_NOT_FOUND,
1674 ccf::errors::ProposalNotFound,
1675 fmt::format("Could not find proposal {}.", proposal_id));
1676 return;
1677 }
1678
1679 if (pi_.value().state != ProposalState::OPEN)
1680 {
1681 set_gov_error(
1682 ctx.rpc_ctx,
1683 HTTP_STATUS_BAD_REQUEST,
1684 ccf::errors::ProposalNotOpen,
1685 fmt::format(
1686 "Proposal {} is currently in state {} - only {} proposals can "
1687 "receive votes.",
1688 proposal_id,
1689 pi_.value().state,
1691 return;
1692 }
1693
1694 auto pm = ctx.tx.ro<ccf::jsgov::ProposalMap>(jsgov::Tables::PROPOSALS);
1695 auto p = pm->get(proposal_id);
1696
1697 if (!p)
1698 {
1699 set_gov_error(
1700 ctx.rpc_ctx,
1701 HTTP_STATUS_NOT_FOUND,
1702 ccf::errors::ProposalNotFound,
1703 fmt::format("Proposal {} does not exist.", proposal_id));
1704 return;
1705 }
1706
1707 if (pi_->ballots.find(member_id.value()) != pi_->ballots.end())
1708 {
1709 set_gov_error(
1710 ctx.rpc_ctx,
1711 HTTP_STATUS_BAD_REQUEST,
1712 ccf::errors::VoteAlreadyExists,
1713 "Vote already submitted.");
1714 return;
1715 }
1716 // Validate vote
1717
1718 auto params = nlohmann::json::parse(
1719 cose_auth_id.has_value() ? cose_auth_id->content :
1720 ctx.rpc_ctx->get_request_body());
1721
1722 {
1724 const auto options_handle =
1725 ctx.tx.ro<ccf::JSEngine>(ccf::Tables::JSENGINE);
1726 context.runtime().set_runtime_options(
1727 options_handle->get(),
1729 auto ballot_func = context.get_exported_function(
1730 params["ballot"], "vote", "body[\"ballot\"]");
1731 }
1732
1733 pi_->ballots[member_id.value()] = params["ballot"];
1734 pi->put(proposal_id, pi_.value());
1735
1736 if (cose_auth_id.has_value())
1737 {
1738 record_cose_governance_history(
1739 ctx.tx, member_id.value(), cose_auth_id->envelope);
1740 }
1741
1742 auto rv = resolve_proposal(
1743 ctx.tx, proposal_id, p.value(), constitution.value());
1744 if (rv.state == ProposalState::FAILED)
1745 {
1746 // If the proposal failed to apply, we want to discard the tx and not
1747 // apply its side-effects to the KV state.
1748 set_gov_error(
1749 ctx.rpc_ctx,
1750 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1751 ccf::errors::InternalError,
1752 fmt::format("{}", rv.failure));
1753 return;
1754 }
1755 else
1756 {
1757 pi_.value().state = rv.state;
1758 pi_.value().final_votes = rv.votes;
1759 pi_.value().vote_failures = rv.vote_failures;
1760 pi_.value().failure = rv.failure;
1761 pi->put(proposal_id, pi_.value());
1762 ctx.rpc_ctx->set_response_header(
1763 ccf::http::headers::CONTENT_TYPE,
1764 http::headervalues::contenttype::JSON);
1765 ctx.rpc_ctx->set_response_body(nlohmann::json(rv).dump());
1766 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1767 return;
1768 }
1769 };
1771 "/proposals/{proposal_id}/ballots",
1772 HTTP_POST,
1773 vote_js,
1774 member_sig_only_policies("ballot"))
1776 .set_openapi_summary(
1777 "Submit a ballot for a proposed change to the service")
1778 .set_openapi_deprecated_replaced(
1779 "5.0.0",
1780 "POST /gov/members/proposals/{proposalId}/ballots/{memberId}:submit")
1781 .install();
1782
1783 auto get_vote_js =
1784 [this](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) {
1785 std::string error;
1786 ProposalId proposal_id;
1787 if (!get_proposal_id_from_path(
1788 ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
1789 {
1790 return make_error(
1791 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
1792 }
1793
1794 MemberId vote_member_id;
1795 if (!get_member_id_from_path(
1796 ctx.rpc_ctx->get_request_path_params(), vote_member_id, error))
1797 {
1798 return make_error(
1799 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
1800 }
1801
1802 auto pi = ctx.tx.ro<ccf::jsgov::ProposalInfoMap>(
1803 jsgov::Tables::PROPOSALS_INFO);
1804 auto pi_ = pi->get(proposal_id);
1805 if (!pi_)
1806 {
1807 return make_error(
1808 HTTP_STATUS_NOT_FOUND,
1809 ccf::errors::ProposalNotFound,
1810 fmt::format("Proposal {} does not exist.", proposal_id));
1811 }
1812
1813 const auto vote_it = pi_->ballots.find(vote_member_id);
1814 if (vote_it == pi_->ballots.end())
1815 {
1816 return make_error(
1817 HTTP_STATUS_NOT_FOUND,
1818 ccf::errors::VoteNotFound,
1819 fmt::format(
1820 "Member {} has not voted for proposal {}.",
1821 vote_member_id,
1822 proposal_id));
1823 }
1824
1825 return make_success(jsgov::Ballot{vote_it->second});
1826 };
1828 "/proposals/{proposal_id}/ballots/{member_id}",
1829 HTTP_GET,
1830 json_read_only_adapter(get_vote_js),
1831 ccf::no_auth_required)
1833 .set_openapi_summary(
1834 "Ballot for a given member about a proposed change to the service")
1835 .set_openapi_deprecated_replaced(
1836 "5.0.0", "GET /gov/members/proposals/{proposalId}/ballots/{memberId}")
1837 .install();
1838
1839 using AllMemberDetails = std::map<ccf::MemberId, FullMemberDetails>;
1840 auto get_all_members =
1841 [this](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) {
1842 auto members = ctx.tx.ro<ccf::MemberInfo>(ccf::Tables::MEMBER_INFO);
1843 auto member_certs =
1844 ctx.tx.ro<ccf::MemberCerts>(ccf::Tables::MEMBER_CERTS);
1845 auto member_public_encryption_keys =
1847 ccf::Tables::MEMBER_ENCRYPTION_PUBLIC_KEYS);
1848
1849 AllMemberDetails response;
1850
1851 members->foreach(
1852 [&response, member_certs, member_public_encryption_keys](
1853 const auto& k, const auto& v) {
1855 md.status = v.status;
1856 md.member_data = v.member_data;
1857
1858 const auto cert = member_certs->get(k);
1859 if (cert.has_value())
1860 {
1861 md.cert = cert.value();
1862 }
1863
1864 const auto public_encryption_key =
1865 member_public_encryption_keys->get(k);
1866 if (public_encryption_key.has_value())
1867 {
1868 md.public_encryption_key = public_encryption_key.value();
1869 }
1870
1871 response[k] = md;
1872 return true;
1873 });
1874
1875 return make_success(response);
1876 };
1878 "/members",
1879 HTTP_GET,
1880 json_read_only_adapter(get_all_members),
1881 ccf::no_auth_required)
1882 .set_auto_schema<void, AllMemberDetails>()
1883 .set_openapi_deprecated_replaced("5.0.0", "GET /gov/service/members")
1884 .install();
1885
1886 add_kv_wrapper_endpoints();
1887 }
1888
1889 bool request_needs_root(const RpcContext& rpc_ctx) override
1890 {
1892 (rpc_ctx.get_request_verb() == HTTP_POST &&
1893 rpc_ctx.get_request_path() == "/gov/proposals");
1894 }
1895 };
1896
1898 {
1899 protected:
1901
1902 public:
1904 NetworkState& network, ccf::AbstractNodeContext& context) :
1905 RpcFrontend(*network.tables, member_endpoints, context),
1906 member_endpoints(network, context)
1907 {}
1908 };
1909} // namespace ccf
Definition gov_effects_interface.h:12
Definition node_operation_interface.h:22
ccf::AbstractNodeContext & context
Definition base_endpoint_registry.h:123
Definition gov_endpoint_registry.h:21
void init_handlers() override
Definition gov_endpoint_registry.h:34
bool request_needs_root(const RpcContext &rpc_ctx) override
Definition gov_endpoint_registry.h:46
static std::optional< ServiceStatus > get_service_status(ccf::kv::ReadOnlyTx &tx)
Definition internal_tables_access.h:401
static bool is_recovery_member(ccf::kv::ReadOnlyTx &tx, const MemberId &member_id)
Definition internal_tables_access.h:56
static bool activate_member(ccf::kv::Tx &tx, const MemberId &member_id)
Definition internal_tables_access.h:152
static size_t get_recovery_threshold(ccf::kv::ReadOnlyTx &tx)
Definition internal_tables_access.h:583
Definition member_frontend.h:95
AuthnPolicies member_cert_or_sig_policies(const std::string &gov_msg_type)
Definition member_frontend.h:667
void init_handlers() override
Definition member_frontend.h:674
MemberEndpoints(NetworkState &network_, ccf::AbstractNodeContext &context_)
Definition member_frontend.h:592
bool request_needs_root(const RpcContext &rpc_ctx) override
Definition member_frontend.h:1889
AuthnPolicies member_sig_only_policies(const std::string &gov_msg_type)
Definition member_frontend.h:662
static std::optional< MemberId > get_caller_member_id(endpoints::CommandEndpointContext &ctx)
Definition member_frontend.h:605
bool authnz_active_member(ccf::endpoints::EndpointContext &ctx, std::optional< MemberId > &member_id, std::optional< ccf::MemberCOSESign1AuthnIdentity > &cose_auth_id, bool must_be_active=true)
Definition member_frontend.h:625
Definition member_frontend.h:1898
MemberRpcFrontend(NetworkState &network, ccf::AbstractNodeContext &context)
Definition member_frontend.h:1903
MemberEndpoints member_endpoints
Definition member_frontend.h:1900
Describes the currently executing RPC.
Definition rpc_context.h:58
Definition frontend.h:36
ccf::kv::Store & tables
Definition frontend.h:38
Definition share_manager.h:155
static std::optional< EncryptedShare > get_encrypted_share(ccf::kv::ReadOnlyTx &tx, const MemberId &member_id)
Definition share_manager.h:437
size_t submit_recovery_share(ccf::kv::Tx &tx, MemberId member_id, const std::vector< uint8_t > &submitted_recovery_share)
Definition share_manager.h:545
static void clear_submitted_recovery_shares(ccf::kv::Tx &tx)
Definition share_manager.h:567
Definition pem.h:18
Definition sha256_hash.h:16
std::string hex_str() const
Definition sha256_hash.cpp:61
virtual Endpoint make_endpoint(const std::string &method, RESTVerb verb, const EndpointFunction &f, const AuthnPolicies &ap)
Definition endpoint_registry.cpp:204
struct ccf::endpoints::EndpointRegistry::OpenApiInfo openapi_info
bool get_path_param(const ccf::PathParams &params, const std::string &param_name, T &value, std::string &error)
Definition endpoint_registry.h:135
virtual Endpoint make_read_only_endpoint(const std::string &method, RESTVerb verb, const ReadOnlyEndpointFunction &f, const AuthnPolicies &ap)
Definition endpoint_registry.cpp:235
Definition context.h:46
Definition tx.h:161
M::ReadOnlyHandle * ro(M &m)
Definition tx.h:170
Definition tx.h:202
M::Handle * rw(M &m)
Definition tx.h:213
#define GOV_FAIL_FMT
Definition gov_logging.h:16
#define GOV_DEBUG_FMT
Definition gov_logging.h:9
#define GOV_TRACE_FMT
Definition gov_logging.h:8
#define GOV_INFO_FMT
Definition gov_logging.h:15
virtual std::string get_request_path() const =0
virtual const ccf::RESTVerb & get_request_verb() const =0
llhttp_status http_status
Definition http_status.h:7
#define DECLARE_JSON_REQUIRED_FIELDS(TYPE,...)
Definition json.h:712
#define DECLARE_JSON_TYPE(TYPE)
Definition json.h:661
ccf::crypto::Pem cert_der_to_pem(const std::vector< uint8_t > &der)
Definition verifier.cpp:33
std::vector< uint8_t > raw_from_b64(const std::string_view &b64_string)
Definition base64.cpp:12
std::string b64_from_raw(const uint8_t *data, size_t size)
Definition base64.cpp:39
HashBytes sha256(const std::span< uint8_t const > &data)
Definition hash.cpp:24
WithKVExtension< CommonContext > CommonContextWithLocalTx
Definition common_context.h:60
ccf::kv::RawCopySerialisedMap< ccf::ProposalId, std::vector< uint8_t > > ProposalMap
Definition gov.h:83
std::unordered_map< ccf::MemberId, Failure > VoteFailures
Definition gov.h:32
ServiceMap< ccf::ProposalId, ProposalInfo > ProposalInfoMap
Definition gov.h:84
Definition app_interface.h:15
ccf::kv::RawCopySerialisedMap< MemberId, ccf::crypto::Pem > MemberCerts
Definition members.h:79
std::string JwtIssuer
Definition jwt.h:57
ServiceMap< MemberId, MemberDetails > MemberInfo
Definition members.h:77
@ error
Definition tls_session.h:25
std::map< std::string, std::string, std::less<> > PathParams
Definition rpc_context.h:54
jsonhandler::JsonAdapterResponse make_success()
Definition json_handler.cpp:102
std::vector< std::shared_ptr< AuthnPolicy > > AuthnPolicies
Definition authentication_types.h:47
std::string ProposalId
Definition proposals.h:40
ccf::kv::RawCopySerialisedMap< MemberId, ccf::crypto::Pem > MemberPublicEncryptionKeys
Definition members.h:81
std::string Module
Definition modules.h:14
ProposalSubmissionStatus
Definition member_frontend.h:88
jsonhandler::JsonAdapterResponse make_error(http_status status, const std::string &code, const std::string &msg)
Definition json_handler.cpp:118
endpoints::ReadOnlyEndpointFunction json_read_only_adapter(const ReadOnlyHandlerWithJson &f)
Definition json_handler.cpp:139
endpoints::EndpointFunction json_adapter(const HandlerJsonParamsAndForward &f)
Definition json_handler.cpp:131
ServiceValue< JSRuntimeOptions > JSEngine
Definition jsengine.h:106
std::map< std::string, ccf::endpoints::EndpointProperties > JsBundleEndpoint
Definition member_frontend.h:53
Definition consensus_types.h:23
Definition map_serializers.h:11
STL namespace.
Definition node_context.h:12
std::shared_ptr< T > get_subsystem(const std::string &name) const
Definition node_context.h:37
Value & value()
Definition entity_id.h:60
Definition member_frontend.h:79
ccf::crypto::Pem cert
Definition member_frontend.h:80
std::optional< ccf::crypto::Pem > public_encryption_key
Definition member_frontend.h:81
Definition call_types.h:128
Definition call_types.h:124
Definition member_frontend.h:56
std::map< std::string, JsBundleEndpoint > endpoints
Definition member_frontend.h:57
Definition member_frontend.h:63
JsBundleMetadata metadata
Definition member_frontend.h:64
std::vector< SetModule > modules
Definition member_frontend.h:65
Definition member_frontend.h:71
ccf::crypto::Pem cert
Definition member_frontend.h:73
JwtIssuer issuer
Definition member_frontend.h:72
Definition members.h:109
Definition cose_auth.h:59
Definition cert_auth.h:58
Definition members.h:61
nlohmann::json member_data
Definition members.h:66
MemberStatus status
Status of the member in the consortium.
Definition members.h:63
Definition network_state.h:12
const MemberInfo member_info
Definition network_tables.h:62
const MemberAcks member_acks
Definition network_tables.h:63
const Configuration config
Definition network_tables.h:182
const COSEGovernanceHistory cose_governance_history
Definition network_tables.h:111
const COSERecentProposals cose_recent_proposals
Definition network_tables.h:113
const Signatures signatures
Definition network_tables.h:219
const GovernanceHistory governance_history
Definition network_tables.h:110
auto get_all_builtin_governance_tables() const
Definition network_tables.h:193
const JwtPublicSigningKeys jwt_public_signing_keys_metadata
Definition network_tables.h:158
const Constitution constitution
Definition network_tables.h:183
Definition member_frontend.h:45
std::string name
Definition member_frontend.h:46
Definition signed_req.h:13
Definition members.h:95
Definition call_types.h:141
Definition call_types.h:134
Definition endpoint_context.h:24
std::shared_ptr< ccf::RpcContext > rpc_ctx
Definition endpoint_context.h:31
const T * try_get_caller()
Definition endpoint_context.h:35
Definition endpoint_context.h:55
ccf::kv::Tx & tx
Definition endpoint_context.h:61
Definition endpoint.h:20
std::string document_version
Definition endpoint_registry.h:131
std::string title
Definition endpoint_registry.h:129
std::string description
Definition endpoint_registry.h:130
Endpoint & set_openapi_deprecated_replaced(const std::string &deprecation_version, const std::string &replacement)
Definition endpoint.cpp:109
void install()
Definition endpoint.cpp:120
Endpoint & set_auto_schema(std::optional< http_status > status=std::nullopt)
Definition endpoint.h:345
Endpoint & set_openapi_summary(const std::string &summary)
Definition endpoint.cpp:97
Definition endpoint_context.h:70
ccf::kv::ReadOnlyTx & tx
Definition endpoint_context.h:77
Definition gov.h:108
Definition gov.h:17
Proposal summary constructed while executing/resolving proposal ballots.
Definition gov.h:67
ccf::MemberId proposer_id
Definition gov.h:69
Proposal metadata stored in the KV.
Definition gov.h:36
ccf::ProposalState state
Current state of this proposal (eg - open, accepted, withdrawn)
Definition gov.h:40
Definition gov.h:101
Definition nonstd.h:29
Definition measurement.h:52