1use std::sync::Arc;
12
13use async_trait::async_trait;
14use http::{Method, StatusCode};
15use percent_encoding::percent_decode_str;
16use serde_json::{json, Map, Value};
17use tokio::sync::Mutex as AsyncMutex;
18
19use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
20use fakecloud_persistence::SnapshotStore;
21
22use crate::persistence::save_snapshot;
23use crate::shared;
24use crate::state::{ManagedBlockchainData, SharedManagedBlockchainState};
25
26pub const MANAGEDBLOCKCHAIN_ACTIONS: &[&str] = &[
28 "CreateAccessor",
29 "CreateMember",
30 "CreateNetwork",
31 "CreateNode",
32 "CreateProposal",
33 "DeleteAccessor",
34 "DeleteMember",
35 "DeleteNode",
36 "GetAccessor",
37 "GetMember",
38 "GetNetwork",
39 "GetNode",
40 "GetProposal",
41 "ListAccessors",
42 "ListInvitations",
43 "ListMembers",
44 "ListNetworks",
45 "ListNodes",
46 "ListProposalVotes",
47 "ListProposals",
48 "ListTagsForResource",
49 "RejectInvitation",
50 "TagResource",
51 "UntagResource",
52 "UpdateMember",
53 "UpdateNode",
54 "VoteOnProposal",
55];
56
57const MUTATING: &[&str] = &[
59 "CreateAccessor",
60 "CreateMember",
61 "CreateNetwork",
62 "CreateNode",
63 "CreateProposal",
64 "DeleteAccessor",
65 "DeleteMember",
66 "DeleteNode",
67 "RejectInvitation",
68 "TagResource",
69 "UntagResource",
70 "UpdateMember",
71 "UpdateNode",
72 "VoteOnProposal",
73];
74
75pub struct ManagedBlockchainService {
76 state: SharedManagedBlockchainState,
77 snapshot_store: Option<Arc<dyn SnapshotStore>>,
78 snapshot_lock: Arc<AsyncMutex<()>>,
79}
80
81impl ManagedBlockchainService {
82 pub fn new(state: SharedManagedBlockchainState) -> Self {
83 Self {
84 state,
85 snapshot_store: None,
86 snapshot_lock: Arc::new(AsyncMutex::new(())),
87 }
88 }
89
90 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
91 self.snapshot_store = Some(store);
92 self
93 }
94
95 async fn save(&self) {
96 save_snapshot(
97 &self.state,
98 self.snapshot_store.clone(),
99 &self.snapshot_lock,
100 )
101 .await;
102 }
103
104 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
106 let store = self.snapshot_store.clone()?;
107 let state = self.state.clone();
108 let lock = self.snapshot_lock.clone();
109 Some(Arc::new(move || {
110 let state = state.clone();
111 let store = store.clone();
112 let lock = lock.clone();
113 Box::pin(async move {
114 save_snapshot(&state, Some(store), &lock).await;
115 })
116 }))
117 }
118
119 fn reconcile(&self, account: &str) -> bool {
124 let now = shared::iso_now();
125 let mut guard = self.state.write();
126 let data = guard.get_or_create(account);
127 let mut changed = false;
128 for m in data.members.values_mut() {
129 if promote_creating(m) {
130 changed = true;
131 }
132 }
133 for n in data.nodes.values_mut() {
134 if promote_creating(n) {
135 changed = true;
136 }
137 }
138 for p in data.proposals.values_mut() {
139 if let Some(obj) = p.as_object_mut() {
140 let status = obj.get("Status").and_then(Value::as_str).unwrap_or("");
141 let expired = obj
142 .get("ExpirationDate")
143 .and_then(Value::as_str)
144 .map(|e| e < now.as_str())
145 .unwrap_or(false);
146 if status == "IN_PROGRESS" && expired {
147 obj.insert("Status".into(), json!("EXPIRED"));
148 changed = true;
149 }
150 }
151 }
152 changed
153 }
154
155 fn resolve_action(req: &AwsRequest) -> Option<(&'static str, Vec<String>)> {
158 let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
159 let trimmed = raw.strip_prefix('/').unwrap_or(raw);
160 let segs: Vec<String> = if trimmed.is_empty() {
161 Vec::new()
162 } else {
163 trimmed
164 .split('/')
165 .map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned())
166 .collect()
167 };
168 let s: Vec<&str> = segs.iter().map(String::as_str).collect();
169 let m = &req.method;
170 let get = m == Method::GET;
171 let post = m == Method::POST;
172 let del = m == Method::DELETE;
173 let patch = m == Method::PATCH;
174 let l = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
175 let (action, labels): (&'static str, Vec<String>) = match s.as_slice() {
176 ["networks"] if post => ("CreateNetwork", vec![]),
177 ["networks"] if get => ("ListNetworks", vec![]),
178 ["networks", nid] if get => ("GetNetwork", l(&[nid])),
179 ["networks", nid, "members"] if post => ("CreateMember", l(&[nid])),
180 ["networks", nid, "members"] if get => ("ListMembers", l(&[nid])),
181 ["networks", nid, "members", mid] if get => ("GetMember", l(&[nid, mid])),
182 ["networks", nid, "members", mid] if patch => ("UpdateMember", l(&[nid, mid])),
183 ["networks", nid, "members", mid] if del => ("DeleteMember", l(&[nid, mid])),
184 ["networks", nid, "nodes"] if post => ("CreateNode", l(&[nid])),
185 ["networks", nid, "nodes"] if get => ("ListNodes", l(&[nid])),
186 ["networks", nid, "nodes", ndid] if get => ("GetNode", l(&[nid, ndid])),
187 ["networks", nid, "nodes", ndid] if patch => ("UpdateNode", l(&[nid, ndid])),
188 ["networks", nid, "nodes", ndid] if del => ("DeleteNode", l(&[nid, ndid])),
189 ["networks", nid, "proposals"] if post => ("CreateProposal", l(&[nid])),
190 ["networks", nid, "proposals"] if get => ("ListProposals", l(&[nid])),
191 ["networks", nid, "proposals", pid] if get => ("GetProposal", l(&[nid, pid])),
192 ["networks", nid, "proposals", pid, "votes"] if get => {
193 ("ListProposalVotes", l(&[nid, pid]))
194 }
195 ["networks", nid, "proposals", pid, "votes"] if post => {
196 ("VoteOnProposal", l(&[nid, pid]))
197 }
198 ["accessors"] if post => ("CreateAccessor", vec![]),
199 ["accessors"] if get => ("ListAccessors", vec![]),
200 ["accessors", aid] if get => ("GetAccessor", l(&[aid])),
201 ["accessors", aid] if del => ("DeleteAccessor", l(&[aid])),
202 ["invitations"] if get => ("ListInvitations", vec![]),
203 ["invitations", iid] if del => ("RejectInvitation", l(&[iid])),
204 ["tags", arn] if post => ("TagResource", l(&[arn])),
205 ["tags", arn] if get => ("ListTagsForResource", l(&[arn])),
206 ["tags", arn] if del => ("UntagResource", l(&[arn])),
207 _ => return None,
208 };
209 Some((action, labels))
210 }
211}
212
213#[async_trait]
214impl AwsService for ManagedBlockchainService {
215 fn service_name(&self) -> &str {
216 "managedblockchain"
217 }
218
219 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
220 let Some((action, labels)) = Self::resolve_action(&req) else {
221 return Err(AwsServiceError::aws_error(
222 StatusCode::NOT_FOUND,
223 "ResourceNotFoundException",
224 format!("Unknown operation: {} {}", req.method, req.raw_path),
225 ));
226 };
227 let (result, settled) = self.dispatch(action, &labels, &req);
228 let success = matches!(result.as_ref(), Ok(resp) if resp.status.is_success());
229 if settled || (MUTATING.contains(&action) && success) {
230 self.save().await;
231 }
232 result
233 }
234
235 fn supported_actions(&self) -> &[&str] {
236 MANAGEDBLOCKCHAIN_ACTIONS
237 }
238}
239
240struct Ctx {
242 account: String,
243 region: String,
244}
245
246impl ManagedBlockchainService {
247 fn dispatch(
248 &self,
249 action: &str,
250 labels: &[String],
251 req: &AwsRequest,
252 ) -> (Result<AwsResponse, AwsServiceError>, bool) {
253 let body = match parse_body(req) {
254 Ok(b) => b,
255 Err(e) => return (Err(e), false),
256 };
257 if let Err(e) = crate::validate::validate_input(action, &body) {
258 return (Err(e), false);
259 }
260 let ctx = Ctx {
261 account: req.account_id.clone(),
262 region: req.region.clone(),
263 };
264 let q = parse_query(&req.raw_query);
265 let settled = self.reconcile(&ctx.account);
266 let a = |i: usize| labels.get(i).map(String::as_str).unwrap_or_default();
267 let result = match action {
268 "CreateNetwork" => self.create_network(&ctx, &body),
269 "GetNetwork" => self.get_network(&ctx, a(0)),
270 "ListNetworks" => self.list_networks(&ctx, &q),
271 "CreateMember" => self.create_member(&ctx, a(0), &body),
272 "GetMember" => self.get_member(&ctx, a(0), a(1)),
273 "UpdateMember" => self.update_member(&ctx, a(0), a(1), &body),
274 "DeleteMember" => self.delete_member(&ctx, a(0), a(1)),
275 "ListMembers" => self.list_members(&ctx, a(0), &q),
276 "CreateNode" => self.create_node(&ctx, a(0), &body),
277 "GetNode" => self.get_node(&ctx, a(0), a(1)),
278 "UpdateNode" => self.update_node(&ctx, a(0), a(1), &body),
279 "DeleteNode" => self.delete_node(&ctx, a(0), a(1)),
280 "ListNodes" => self.list_nodes(&ctx, a(0), &q),
281 "CreateProposal" => self.create_proposal(&ctx, a(0), &body),
282 "GetProposal" => self.get_proposal(&ctx, a(0), a(1)),
283 "ListProposals" => self.list_proposals(&ctx, a(0), &q),
284 "ListProposalVotes" => self.list_proposal_votes(&ctx, a(0), a(1), &q),
285 "VoteOnProposal" => self.vote_on_proposal(&ctx, a(0), a(1), &body),
286 "CreateAccessor" => self.create_accessor(&ctx, &body),
287 "GetAccessor" => self.get_accessor(&ctx, a(0)),
288 "DeleteAccessor" => self.delete_accessor(&ctx, a(0)),
289 "ListAccessors" => self.list_accessors(&ctx, &q),
290 "ListInvitations" => self.list_invitations(&ctx, &q),
291 "RejectInvitation" => self.reject_invitation(&ctx, a(0)),
292 "TagResource" => self.tag_resource(&ctx, a(0), &body),
293 "UntagResource" => self.untag_resource(&ctx, a(0), &q),
294 "ListTagsForResource" => self.list_tags(&ctx, a(0)),
295 _ => Err(AwsServiceError::action_not_implemented(
296 "managedblockchain",
297 action,
298 )),
299 };
300 (result, settled)
301 }
302
303 fn create_network(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
306 let now = shared::iso_now();
307 let network_id = shared::new_network_id();
308 let framework = str_or(body, "Framework", "HYPERLEDGER_FABRIC");
309 let member_id = shared::new_member_id();
310
311 let mut net = Map::new();
312 net.insert("Id".into(), json!(network_id));
313 net.insert("Name".into(), json!(str_or(body, "Name", "")));
314 net.insert("Framework".into(), json!(framework));
315 net.insert(
316 "FrameworkVersion".into(),
317 json!(str_or(body, "FrameworkVersion", "")),
318 );
319 net.insert("Status".into(), json!("AVAILABLE"));
320 net.insert("CreationDate".into(), json!(now));
321 net.insert("Arn".into(), json!(shared::network_arn(&network_id)));
322 net.insert(
323 "VpcEndpointServiceName".into(),
324 json!(format!(
325 "com.amazonaws.{}.managedblockchain.{}",
326 ctx.region,
327 network_id.to_lowercase()
328 )),
329 );
330 echo(&mut net, body, &["Description", "VotingPolicy"]);
331 net.insert(
332 "FrameworkAttributes".into(),
333 network_framework_attributes(&framework, &network_id, body, &ctx.region),
334 );
335
336 let is_fabric = framework == "HYPERLEDGER_FABRIC";
337
338 let mut guard = self.state.write();
339 let data = guard.get_or_create(&ctx.account);
340 store_tags(data, &shared::network_arn(&network_id), body);
341 data.networks.insert(network_id.clone(), Value::Object(net));
342
343 let mut out = json!({ "NetworkId": network_id });
346 if is_fabric {
347 let member_config = body
348 .get("MemberConfiguration")
349 .cloned()
350 .unwrap_or(json!({}));
351 let member = build_member(ctx, &network_id, &member_id, &member_config, &now, true);
352 let arn = shared::member_arn(&ctx.region, &ctx.account, &member_id);
353 store_tags(data, &arn, &member_config);
354 data.members.insert(member_id.clone(), member);
355 out.as_object_mut()
356 .unwrap()
357 .insert("MemberId".into(), json!(member_id));
358 }
359 ok(out)
360 }
361
362 fn get_network(&self, ctx: &Ctx, network_id: &str) -> Result<AwsResponse, AwsServiceError> {
363 let guard = self.state.read();
364 let data = guard.get(&ctx.account);
365 match data.and_then(|d| d.networks.get(network_id)) {
366 Some(n) => {
367 let arn = shared::network_arn(network_id);
368 ok(json!({ "Network": with_tags(data.unwrap(), &arn, n) }))
369 }
370 None => Err(not_found(&format!("Network {network_id} was not found."))),
371 }
372 }
373
374 fn list_networks(
375 &self,
376 ctx: &Ctx,
377 q: &[(String, String)],
378 ) -> Result<AwsResponse, AwsServiceError> {
379 validate_list_query(
380 q,
381 10,
382 &[
383 ("framework", crate::validate::FRAMEWORK),
384 ("status", NETWORK_STATUS),
385 ],
386 )?;
387 let name = query_one(q, "name");
388 let framework = query_one(q, "framework");
389 let status = query_one(q, "status");
390 let guard = self.state.read();
391 let mut networks: Vec<Value> = guard
392 .get(&ctx.account)
393 .map(|d| {
394 d.networks
395 .values()
396 .filter(|n| {
397 opt_eq(n, "Name", name)
398 && opt_eq(n, "Framework", framework)
399 && opt_eq(n, "Status", status)
400 })
401 .map(network_summary)
402 .collect()
403 })
404 .unwrap_or_default();
405 networks.sort_by(|a, b| summary_id(a).cmp(summary_id(b)));
406 let (page, next) = paginate(networks, q)?;
407 let mut out = json!({ "Networks": page });
408 if let Some(n) = next {
409 out["NextToken"] = json!(n);
410 }
411 ok(out)
412 }
413
414 fn create_member(
417 &self,
418 ctx: &Ctx,
419 network_id: &str,
420 body: &Value,
421 ) -> Result<AwsResponse, AwsServiceError> {
422 check_id(network_id, "NetworkId")?;
423 let invitation_id = str_or(body, "InvitationId", "");
424 let now = shared::iso_now();
425 let member_id = shared::new_member_id();
426 let member_config = body
427 .get("MemberConfiguration")
428 .cloned()
429 .unwrap_or(json!({}));
430
431 let mut guard = self.state.write();
432 let data = guard.get_or_create(&ctx.account);
433 if !data.networks.contains_key(network_id) {
434 return Err(not_found(&format!("Network {network_id} was not found.")));
435 }
436 match data
440 .invitations
441 .get(&invitation_id)
442 .and_then(|inv| inv.get("Status"))
443 .and_then(Value::as_str)
444 {
445 Some("PENDING") => {
446 if let Some(obj) = data
447 .invitations
448 .get_mut(&invitation_id)
449 .and_then(Value::as_object_mut)
450 {
451 obj.insert("Status".into(), json!("ACCEPTED"));
452 }
453 }
454 _ => {
455 return Err(invalid_request(&format!(
456 "Invitation {invitation_id} was not found or is not pending."
457 )));
458 }
459 }
460 let member = build_member(ctx, network_id, &member_id, &member_config, &now, true);
461 let arn = shared::member_arn(&ctx.region, &ctx.account, &member_id);
462 store_tags(data, &arn, &member_config);
463 data.members.insert(member_id.clone(), member);
464 ok(json!({ "MemberId": member_id }))
465 }
466
467 fn get_member(
468 &self,
469 ctx: &Ctx,
470 _network_id: &str,
471 member_id: &str,
472 ) -> Result<AwsResponse, AwsServiceError> {
473 let guard = self.state.read();
474 let data = guard.get(&ctx.account);
475 match data.and_then(|d| d.members.get(member_id)) {
476 Some(m) => {
477 let arn = shared::member_arn(&ctx.region, &ctx.account, member_id);
478 ok(json!({ "Member": with_tags(data.unwrap(), &arn, m) }))
479 }
480 None => Err(not_found(&format!("Member {member_id} was not found."))),
481 }
482 }
483
484 fn update_member(
485 &self,
486 ctx: &Ctx,
487 _network_id: &str,
488 member_id: &str,
489 body: &Value,
490 ) -> Result<AwsResponse, AwsServiceError> {
491 let mut guard = self.state.write();
492 let data = guard.get_or_create(&ctx.account);
493 let Some(m) = data
494 .members
495 .get_mut(member_id)
496 .and_then(Value::as_object_mut)
497 else {
498 return Err(not_found(&format!("Member {member_id} was not found.")));
499 };
500 if let Some(lpc) = body.get("LogPublishingConfiguration") {
501 m.insert("LogPublishingConfiguration".into(), lpc.clone());
502 }
503 ok_empty()
504 }
505
506 fn delete_member(
507 &self,
508 ctx: &Ctx,
509 _network_id: &str,
510 member_id: &str,
511 ) -> Result<AwsResponse, AwsServiceError> {
512 let mut guard = self.state.write();
513 let data = guard.get_or_create(&ctx.account);
514 let Some(m) = data
515 .members
516 .get_mut(member_id)
517 .and_then(Value::as_object_mut)
518 else {
519 return Err(not_found(&format!("Member {member_id} was not found.")));
520 };
521 m.insert("Status".into(), json!("DELETED"));
522 ok_empty()
523 }
524
525 fn list_members(
526 &self,
527 ctx: &Ctx,
528 network_id: &str,
529 q: &[(String, String)],
530 ) -> Result<AwsResponse, AwsServiceError> {
531 check_id(network_id, "NetworkId")?;
532 validate_list_query(q, 20, &[("status", MEMBER_STATUS)])?;
533 let name = query_one(q, "name");
534 let status = query_one(q, "status");
535 let guard = self.state.read();
536 let mut members: Vec<Value> = guard
537 .get(&ctx.account)
538 .map(|d| {
539 d.members
540 .values()
541 .filter(|m| {
542 opt_eq(m, "NetworkId", Some(network_id))
543 && opt_eq(m, "Name", name)
544 && opt_eq(m, "Status", status)
545 })
546 .map(|m| member_summary(m, &ctx.account, &ctx.region))
547 .collect()
548 })
549 .unwrap_or_default();
550 members.sort_by(|a, b| summary_id(a).cmp(summary_id(b)));
551 let (page, next) = paginate(members, q)?;
552 let mut out = json!({ "Members": page });
553 if let Some(n) = next {
554 out["NextToken"] = json!(n);
555 }
556 ok(out)
557 }
558
559 fn create_node(
562 &self,
563 ctx: &Ctx,
564 network_id: &str,
565 body: &Value,
566 ) -> Result<AwsResponse, AwsServiceError> {
567 check_id(network_id, "NetworkId")?;
568 let now = shared::iso_now();
569 let node_id = shared::new_node_id();
570 let member_id = body
571 .get("MemberId")
572 .and_then(Value::as_str)
573 .map(str::to_string);
574 let node_config = body.get("NodeConfiguration").cloned().unwrap_or(json!({}));
575
576 let mut guard = self.state.write();
577 let data = guard.get_or_create(&ctx.account);
578 if !data.networks.contains_key(network_id) {
579 return Err(not_found(&format!("Network {network_id} was not found.")));
580 }
581 let framework = data
582 .networks
583 .get(network_id)
584 .and_then(|n| n.get("Framework"))
585 .and_then(Value::as_str)
586 .unwrap_or("HYPERLEDGER_FABRIC")
587 .to_string();
588 let node = build_node(
589 ctx,
590 network_id,
591 member_id.as_deref(),
592 &node_id,
593 &node_config,
594 &framework,
595 &now,
596 );
597 let arn = shared::node_arn(&ctx.region, &ctx.account, &node_id);
598 store_tags(data, &arn, body);
599 data.nodes.insert(node_id.clone(), node);
600 ok(json!({ "NodeId": node_id }))
601 }
602
603 fn get_node(
604 &self,
605 ctx: &Ctx,
606 _network_id: &str,
607 node_id: &str,
608 ) -> Result<AwsResponse, AwsServiceError> {
609 let guard = self.state.read();
610 let data = guard.get(&ctx.account);
611 match data.and_then(|d| d.nodes.get(node_id)) {
612 Some(n) => {
613 let arn = shared::node_arn(&ctx.region, &ctx.account, node_id);
614 ok(json!({ "Node": with_tags(data.unwrap(), &arn, n) }))
615 }
616 None => Err(not_found(&format!("Node {node_id} was not found."))),
617 }
618 }
619
620 fn update_node(
621 &self,
622 ctx: &Ctx,
623 _network_id: &str,
624 node_id: &str,
625 body: &Value,
626 ) -> Result<AwsResponse, AwsServiceError> {
627 let mut guard = self.state.write();
628 let data = guard.get_or_create(&ctx.account);
629 let Some(n) = data.nodes.get_mut(node_id).and_then(Value::as_object_mut) else {
630 return Err(not_found(&format!("Node {node_id} was not found.")));
631 };
632 if let Some(lpc) = body.get("LogPublishingConfiguration") {
633 n.insert("LogPublishingConfiguration".into(), lpc.clone());
634 }
635 ok_empty()
636 }
637
638 fn delete_node(
639 &self,
640 ctx: &Ctx,
641 _network_id: &str,
642 node_id: &str,
643 ) -> Result<AwsResponse, AwsServiceError> {
644 let mut guard = self.state.write();
645 let data = guard.get_or_create(&ctx.account);
646 let Some(n) = data.nodes.get_mut(node_id).and_then(Value::as_object_mut) else {
647 return Err(not_found(&format!("Node {node_id} was not found.")));
648 };
649 n.insert("Status".into(), json!("DELETED"));
650 ok_empty()
651 }
652
653 fn list_nodes(
654 &self,
655 ctx: &Ctx,
656 network_id: &str,
657 q: &[(String, String)],
658 ) -> Result<AwsResponse, AwsServiceError> {
659 check_id(network_id, "NetworkId")?;
660 validate_list_query(q, 20, &[("status", NODE_STATUS)])?;
661 if let Some((_, mid)) = q.iter().find(|(k, _)| k == "memberId") {
665 check_id(mid, "MemberId")?;
666 }
667 let member_id = query_one(q, "memberId");
668 let status = query_one(q, "status");
669 let guard = self.state.read();
670 let mut nodes: Vec<Value> = guard
671 .get(&ctx.account)
672 .map(|d| {
673 d.nodes
674 .values()
675 .filter(|n| {
676 opt_eq(n, "NetworkId", Some(network_id))
677 && opt_eq(n, "MemberId", member_id)
678 && opt_eq(n, "Status", status)
679 })
680 .map(node_summary)
681 .collect()
682 })
683 .unwrap_or_default();
684 nodes.sort_by(|a, b| summary_id(a).cmp(summary_id(b)));
685 let (page, next) = paginate(nodes, q)?;
686 let mut out = json!({ "Nodes": page });
687 if let Some(n) = next {
688 out["NextToken"] = json!(n);
689 }
690 ok(out)
691 }
692
693 fn create_proposal(
696 &self,
697 ctx: &Ctx,
698 network_id: &str,
699 body: &Value,
700 ) -> Result<AwsResponse, AwsServiceError> {
701 let now = shared::iso_now();
702 let proposal_id = shared::new_proposal_id();
703 let member_id = str_or(body, "MemberId", "");
704 let actions = body.get("Actions").cloned().unwrap_or(json!({}));
705
706 let mut guard = self.state.write();
707 let data = guard.get_or_create(&ctx.account);
708 if !data.networks.contains_key(network_id) {
709 return Err(not_found(&format!("Network {network_id} was not found.")));
710 }
711 let proposed_by_name = data
712 .members
713 .get(&member_id)
714 .and_then(|m| m.get("Name"))
715 .and_then(Value::as_str)
716 .unwrap_or("")
717 .to_string();
718 let duration = proposal_duration_hours(data.networks.get(network_id));
719 let outstanding = eligible_member_count(data, network_id);
720 let expiration = (chrono::Utc::now() + chrono::Duration::hours(duration))
721 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
722 .to_string();
723
724 let mut p = Map::new();
725 p.insert("ProposalId".into(), json!(proposal_id));
726 p.insert("NetworkId".into(), json!(network_id));
727 p.insert("Actions".into(), actions);
728 p.insert("ProposedByMemberId".into(), json!(member_id));
729 p.insert("ProposedByMemberName".into(), json!(proposed_by_name));
730 p.insert("Status".into(), json!("IN_PROGRESS"));
731 p.insert("CreationDate".into(), json!(now));
732 p.insert("ExpirationDate".into(), json!(expiration));
733 p.insert("YesVoteCount".into(), json!(0));
734 p.insert("NoVoteCount".into(), json!(0));
735 p.insert("OutstandingVoteCount".into(), json!(outstanding));
736 p.insert(
737 "Arn".into(),
738 json!(shared::proposal_arn(
739 &ctx.region,
740 &ctx.account,
741 &proposal_id
742 )),
743 );
744 if let Some(d) = body.get("Description") {
745 p.insert("Description".into(), d.clone());
746 }
747 store_tags(
748 data,
749 &shared::proposal_arn(&ctx.region, &ctx.account, &proposal_id),
750 body,
751 );
752 data.proposals.insert(proposal_id.clone(), Value::Object(p));
753 data.votes.insert(proposal_id.clone(), Vec::new());
754 ok(json!({ "ProposalId": proposal_id }))
755 }
756
757 fn get_proposal(
758 &self,
759 ctx: &Ctx,
760 _network_id: &str,
761 proposal_id: &str,
762 ) -> Result<AwsResponse, AwsServiceError> {
763 let guard = self.state.read();
764 let data = guard.get(&ctx.account);
765 match data.and_then(|d| d.proposals.get(proposal_id)) {
766 Some(p) => {
767 let arn = shared::proposal_arn(&ctx.region, &ctx.account, proposal_id);
768 ok(json!({ "Proposal": with_tags(data.unwrap(), &arn, p) }))
769 }
770 None => Err(not_found(&format!("Proposal {proposal_id} was not found."))),
771 }
772 }
773
774 fn list_proposals(
775 &self,
776 ctx: &Ctx,
777 network_id: &str,
778 q: &[(String, String)],
779 ) -> Result<AwsResponse, AwsServiceError> {
780 check_id(network_id, "NetworkId")?;
781 validate_list_query(q, 100, &[])?;
782 let guard = self.state.read();
783 let data = guard.get(&ctx.account);
784 if let Some(d) = data {
785 if !d.networks.contains_key(network_id) {
786 return Err(not_found(&format!("Network {network_id} was not found.")));
787 }
788 }
789 let mut proposals: Vec<Value> = data
790 .map(|d| {
791 d.proposals
792 .values()
793 .filter(|p| opt_eq(p, "NetworkId", Some(network_id)))
794 .map(proposal_summary)
795 .collect()
796 })
797 .unwrap_or_default();
798 proposals.sort_by(|a, b| {
799 a.get("ProposalId")
800 .and_then(Value::as_str)
801 .unwrap_or("")
802 .cmp(b.get("ProposalId").and_then(Value::as_str).unwrap_or(""))
803 });
804 let (page, next) = paginate(proposals, q)?;
805 let mut out = json!({ "Proposals": page });
806 if let Some(n) = next {
807 out["NextToken"] = json!(n);
808 }
809 ok(out)
810 }
811
812 fn list_proposal_votes(
813 &self,
814 ctx: &Ctx,
815 network_id: &str,
816 proposal_id: &str,
817 q: &[(String, String)],
818 ) -> Result<AwsResponse, AwsServiceError> {
819 check_id(network_id, "NetworkId")?;
820 check_id(proposal_id, "ProposalId")?;
821 validate_list_query(q, 100, &[])?;
822 let guard = self.state.read();
823 let votes: Vec<Value> = guard
824 .get(&ctx.account)
825 .and_then(|d| d.votes.get(proposal_id))
826 .cloned()
827 .unwrap_or_default();
828 let (page, next) = paginate(votes, q)?;
829 let mut out = json!({ "ProposalVotes": page });
830 if let Some(n) = next {
831 out["NextToken"] = json!(n);
832 }
833 ok(out)
834 }
835
836 fn vote_on_proposal(
837 &self,
838 ctx: &Ctx,
839 network_id: &str,
840 proposal_id: &str,
841 body: &Value,
842 ) -> Result<AwsResponse, AwsServiceError> {
843 let voter = str_or(body, "VoterMemberId", "");
844 let vote = str_or(body, "Vote", "");
845
846 let mut guard = self.state.write();
847 let mut invitations_to_send: Vec<(String, Value)> = Vec::new();
850 {
851 let region = ctx.region.clone();
852 let data = guard.get_or_create(&ctx.account);
853 let network = data.networks.get(network_id).cloned();
854 if network.is_none() {
855 return Err(not_found(&format!("Network {network_id} was not found.")));
856 }
857 if !data.members.contains_key(&voter) {
858 return Err(not_found(&format!("Member {voter} was not found.")));
859 }
860 let member_name = data
861 .members
862 .get(&voter)
863 .and_then(|m| m.get("Name"))
864 .and_then(Value::as_str)
865 .unwrap_or("")
866 .to_string();
867
868 {
869 let proposal = data
870 .proposals
871 .get(proposal_id)
872 .ok_or_else(|| not_found(&format!("Proposal {proposal_id} was not found.")))?;
873 let status = proposal.get("Status").and_then(Value::as_str).unwrap_or("");
874 if status != "IN_PROGRESS" {
875 return Err(illegal_action(&format!(
876 "Proposal {proposal_id} is not open for voting (status {status})."
877 )));
878 }
879 }
880
881 let votes = data.votes.entry(proposal_id.to_string()).or_default();
882 if votes
883 .iter()
884 .any(|v| v.get("MemberId").and_then(Value::as_str) == Some(voter.as_str()))
885 {
886 return Err(illegal_action(&format!(
887 "Member {voter} has already voted on proposal {proposal_id}."
888 )));
889 }
890 votes.push(json!({
891 "Vote": vote,
892 "MemberId": voter,
893 "MemberName": member_name,
894 }));
895 let yes = votes
896 .iter()
897 .filter(|v| v.get("Vote").and_then(Value::as_str) == Some("YES"))
898 .count() as i64;
899 let no = votes
900 .iter()
901 .filter(|v| v.get("Vote").and_then(Value::as_str) == Some("NO"))
902 .count() as i64;
903
904 let total = eligible_member_count(data, network_id);
905 let (threshold, comparator) = threshold_policy(network.as_ref());
906 let outstanding = (total - yes - no).max(0);
907 let decision = decide_proposal(yes, outstanding, total, threshold, &comparator);
908
909 let actions = data
911 .proposals
912 .get(proposal_id)
913 .and_then(|p| p.get("Actions"))
914 .cloned()
915 .unwrap_or(json!({}));
916 if let Some(p) = data
917 .proposals
918 .get_mut(proposal_id)
919 .and_then(Value::as_object_mut)
920 {
921 p.insert("YesVoteCount".into(), json!(yes));
922 p.insert("NoVoteCount".into(), json!(no));
923 p.insert("OutstandingVoteCount".into(), json!(outstanding));
924 p.insert("Status".into(), json!(decision.clone()));
925 }
926
927 if decision == "APPROVED" {
928 if let Some(removals) = actions.get("Removals").and_then(Value::as_array) {
930 for r in removals {
931 if let Some(mid) = r.get("MemberId").and_then(Value::as_str) {
932 if let Some(m) =
933 data.members.get_mut(mid).and_then(Value::as_object_mut)
934 {
935 m.insert("Status".into(), json!("DELETED"));
936 }
937 }
938 }
939 }
940 if let Some(invites) = actions.get("Invitations").and_then(Value::as_array) {
943 let net = network.as_ref().unwrap();
944 for inv in invites {
945 if let Some(principal) = inv.get("Principal").and_then(Value::as_str) {
946 let invitation = build_invitation(®ion, principal, net);
947 invitations_to_send.push((principal.to_string(), invitation));
948 }
949 }
950 }
951 }
952 }
953
954 for (principal, invitation) in invitations_to_send {
955 let inv_id = invitation
956 .get("InvitationId")
957 .and_then(Value::as_str)
958 .unwrap_or_default()
959 .to_string();
960 let acct = guard.get_or_create(&principal);
961 acct.invitations.insert(inv_id, invitation);
962 }
963
964 ok_empty()
965 }
966
967 fn create_accessor(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
970 let now = shared::iso_now();
971 let accessor_id = shared::new_accessor_id();
972 let billing_token = shared::new_billing_token();
973 let network_type = body
974 .get("NetworkType")
975 .and_then(Value::as_str)
976 .map(str::to_string);
977
978 let mut acc = Map::new();
979 acc.insert("Id".into(), json!(accessor_id));
980 acc.insert(
981 "Type".into(),
982 json!(str_or(body, "AccessorType", "BILLING_TOKEN")),
983 );
984 acc.insert("BillingToken".into(), json!(billing_token));
985 acc.insert("Status".into(), json!("AVAILABLE"));
986 acc.insert("CreationDate".into(), json!(now));
987 acc.insert(
988 "Arn".into(),
989 json!(shared::accessor_arn(
990 &ctx.region,
991 &ctx.account,
992 &accessor_id
993 )),
994 );
995 if let Some(nt) = &network_type {
996 acc.insert("NetworkType".into(), json!(nt));
997 }
998
999 let mut guard = self.state.write();
1000 let data = guard.get_or_create(&ctx.account);
1001 store_tags(
1002 data,
1003 &shared::accessor_arn(&ctx.region, &ctx.account, &accessor_id),
1004 body,
1005 );
1006 data.accessors
1007 .insert(accessor_id.clone(), Value::Object(acc));
1008
1009 let mut out = json!({ "AccessorId": accessor_id, "BillingToken": billing_token });
1010 if let Some(nt) = network_type {
1011 out["NetworkType"] = json!(nt);
1012 }
1013 ok(out)
1014 }
1015
1016 fn get_accessor(&self, ctx: &Ctx, accessor_id: &str) -> Result<AwsResponse, AwsServiceError> {
1017 let guard = self.state.read();
1018 let data = guard.get(&ctx.account);
1019 match data.and_then(|d| d.accessors.get(accessor_id)) {
1020 Some(a) => {
1021 let arn = shared::accessor_arn(&ctx.region, &ctx.account, accessor_id);
1022 ok(json!({ "Accessor": with_tags(data.unwrap(), &arn, a) }))
1023 }
1024 None => Err(not_found(&format!("Accessor {accessor_id} was not found."))),
1025 }
1026 }
1027
1028 fn delete_accessor(
1029 &self,
1030 ctx: &Ctx,
1031 accessor_id: &str,
1032 ) -> Result<AwsResponse, AwsServiceError> {
1033 let mut guard = self.state.write();
1034 let data = guard.get_or_create(&ctx.account);
1035 let Some(a) = data
1036 .accessors
1037 .get_mut(accessor_id)
1038 .and_then(Value::as_object_mut)
1039 else {
1040 return Err(not_found(&format!("Accessor {accessor_id} was not found.")));
1041 };
1042 a.insert("Status".into(), json!("PENDING_DELETION"));
1043 ok_empty()
1044 }
1045
1046 fn list_accessors(
1047 &self,
1048 ctx: &Ctx,
1049 q: &[(String, String)],
1050 ) -> Result<AwsResponse, AwsServiceError> {
1051 validate_list_query(
1052 q,
1053 50,
1054 &[("networkType", crate::validate::ACCESSOR_NETWORK_TYPE)],
1055 )?;
1056 let network_type = query_one(q, "networkType");
1057 let guard = self.state.read();
1058 let mut accessors: Vec<Value> = guard
1059 .get(&ctx.account)
1060 .map(|d| {
1061 d.accessors
1062 .values()
1063 .filter(|a| opt_eq(a, "NetworkType", network_type))
1064 .map(accessor_summary)
1065 .collect()
1066 })
1067 .unwrap_or_default();
1068 accessors.sort_by(|a, b| summary_id(a).cmp(summary_id(b)));
1069 let (page, next) = paginate(accessors, q)?;
1070 let mut out = json!({ "Accessors": page });
1071 if let Some(n) = next {
1072 out["NextToken"] = json!(n);
1073 }
1074 ok(out)
1075 }
1076
1077 fn list_invitations(
1080 &self,
1081 ctx: &Ctx,
1082 q: &[(String, String)],
1083 ) -> Result<AwsResponse, AwsServiceError> {
1084 validate_list_query(q, 100, &[])?;
1085 let guard = self.state.read();
1086 let mut invitations: Vec<Value> = guard
1087 .get(&ctx.account)
1088 .map(|d| d.invitations.values().cloned().collect())
1089 .unwrap_or_default();
1090 invitations.sort_by(|a, b| {
1091 a.get("InvitationId")
1092 .and_then(Value::as_str)
1093 .unwrap_or("")
1094 .cmp(b.get("InvitationId").and_then(Value::as_str).unwrap_or(""))
1095 });
1096 let (page, next) = paginate(invitations, q)?;
1097 let mut out = json!({ "Invitations": page });
1098 if let Some(n) = next {
1099 out["NextToken"] = json!(n);
1100 }
1101 ok(out)
1102 }
1103
1104 fn reject_invitation(
1105 &self,
1106 ctx: &Ctx,
1107 invitation_id: &str,
1108 ) -> Result<AwsResponse, AwsServiceError> {
1109 let mut guard = self.state.write();
1110 let data = guard.get_or_create(&ctx.account);
1111 let Some(inv) = data
1112 .invitations
1113 .get_mut(invitation_id)
1114 .and_then(Value::as_object_mut)
1115 else {
1116 return Err(not_found(&format!(
1117 "Invitation {invitation_id} was not found."
1118 )));
1119 };
1120 let status = inv.get("Status").and_then(Value::as_str).unwrap_or("");
1121 if status != "PENDING" {
1122 return Err(illegal_action(&format!(
1123 "Invitation {invitation_id} is not pending (status {status})."
1124 )));
1125 }
1126 inv.insert("Status".into(), json!("REJECTED"));
1127 ok_empty()
1128 }
1129
1130 fn tag_resource(
1133 &self,
1134 ctx: &Ctx,
1135 arn: &str,
1136 body: &Value,
1137 ) -> Result<AwsResponse, AwsServiceError> {
1138 check_arn(arn)?;
1139 let mut guard = self.state.write();
1140 let data = guard.get_or_create(&ctx.account);
1141 let entry = data.tags.entry(arn.to_string()).or_default();
1142 if let Some(tags) = body.get("Tags").and_then(Value::as_object) {
1143 for (k, v) in tags {
1144 if let Some(s) = v.as_str() {
1145 entry.insert(k.clone(), s.to_string());
1146 }
1147 }
1148 }
1149 ok_empty()
1150 }
1151
1152 fn untag_resource(
1153 &self,
1154 ctx: &Ctx,
1155 arn: &str,
1156 q: &[(String, String)],
1157 ) -> Result<AwsResponse, AwsServiceError> {
1158 check_arn(arn)?;
1159 let mut guard = self.state.write();
1160 let data = guard.get_or_create(&ctx.account);
1161 if let Some(entry) = data.tags.get_mut(arn) {
1162 for (k, v) in q {
1163 if k == "tagKeys" {
1164 entry.remove(v);
1165 }
1166 }
1167 if entry.is_empty() {
1168 data.tags.remove(arn);
1169 }
1170 }
1171 ok_empty()
1172 }
1173
1174 fn list_tags(&self, ctx: &Ctx, arn: &str) -> Result<AwsResponse, AwsServiceError> {
1175 check_arn(arn)?;
1176 let guard = self.state.read();
1177 let tags = guard
1178 .get(&ctx.account)
1179 .and_then(|d| d.tags.get(arn))
1180 .cloned()
1181 .unwrap_or_default();
1182 ok(json!({ "Tags": tags }))
1183 }
1184}
1185
1186fn build_member(
1191 ctx: &Ctx,
1192 network_id: &str,
1193 member_id: &str,
1194 config: &Value,
1195 now: &str,
1196 fabric: bool,
1197) -> Value {
1198 let admin_username = config
1199 .get("FrameworkConfiguration")
1200 .and_then(|f| f.get("Fabric"))
1201 .and_then(|f| f.get("AdminUsername"))
1202 .and_then(Value::as_str)
1203 .unwrap_or("admin")
1204 .to_string();
1205 let mut m = Map::new();
1206 m.insert("NetworkId".into(), json!(network_id));
1207 m.insert("Id".into(), json!(member_id));
1208 m.insert(
1209 "Name".into(),
1210 json!(config.get("Name").and_then(Value::as_str).unwrap_or("")),
1211 );
1212 m.insert("Status".into(), json!("CREATING"));
1213 m.insert("CreationDate".into(), json!(now));
1214 m.insert(
1215 "Arn".into(),
1216 json!(shared::member_arn(&ctx.region, &ctx.account, member_id)),
1217 );
1218 if let Some(d) = config.get("Description") {
1219 m.insert("Description".into(), d.clone());
1220 }
1221 if let Some(lpc) = config.get("LogPublishingConfiguration") {
1222 m.insert("LogPublishingConfiguration".into(), lpc.clone());
1223 }
1224 if let Some(kms) = config.get("KmsKeyArn") {
1225 m.insert("KmsKeyArn".into(), kms.clone());
1226 }
1227 if fabric {
1228 m.insert(
1229 "FrameworkAttributes".into(),
1230 json!({
1231 "Fabric": {
1232 "AdminUsername": admin_username,
1233 "CaEndpoint": shared::fabric_ca_endpoint(member_id, &ctx.region),
1234 }
1235 }),
1236 );
1237 }
1238 Value::Object(m)
1239}
1240
1241fn build_node(
1243 ctx: &Ctx,
1244 network_id: &str,
1245 member_id: Option<&str>,
1246 node_id: &str,
1247 config: &Value,
1248 framework: &str,
1249 now: &str,
1250) -> Value {
1251 let mut n = Map::new();
1252 n.insert("NetworkId".into(), json!(network_id));
1253 if let Some(mid) = member_id {
1254 n.insert("MemberId".into(), json!(mid));
1255 }
1256 n.insert("Id".into(), json!(node_id));
1257 n.insert(
1258 "InstanceType".into(),
1259 json!(config
1260 .get("InstanceType")
1261 .and_then(Value::as_str)
1262 .unwrap_or("")),
1263 );
1264 if let Some(az) = config.get("AvailabilityZone") {
1265 n.insert("AvailabilityZone".into(), az.clone());
1266 } else {
1267 n.insert("AvailabilityZone".into(), json!(format!("{}a", ctx.region)));
1268 }
1269 if let Some(sdb) = config.get("StateDB") {
1270 n.insert("StateDB".into(), sdb.clone());
1271 }
1272 n.insert("Status".into(), json!("CREATING"));
1273 n.insert("CreationDate".into(), json!(now));
1274 n.insert(
1275 "Arn".into(),
1276 json!(shared::node_arn(&ctx.region, &ctx.account, node_id)),
1277 );
1278 if let Some(lpc) = config.get("LogPublishingConfiguration") {
1279 n.insert("LogPublishingConfiguration".into(), lpc.clone());
1280 }
1281 let attrs = if framework == "ETHEREUM" {
1282 json!({
1283 "Ethereum": {
1284 "HttpEndpoint": shared::ethereum_http_endpoint(node_id, &ctx.region),
1285 "WebSocketEndpoint": shared::ethereum_ws_endpoint(node_id, &ctx.region),
1286 }
1287 })
1288 } else {
1289 json!({
1290 "Fabric": {
1291 "PeerEndpoint": shared::fabric_peer_endpoint(node_id, &ctx.region),
1292 "PeerEventEndpoint": shared::fabric_peer_event_endpoint(node_id, &ctx.region),
1293 }
1294 })
1295 };
1296 n.insert("FrameworkAttributes".into(), attrs);
1297 Value::Object(n)
1298}
1299
1300fn build_invitation(region: &str, principal: &str, network: &Value) -> Value {
1302 let id = shared::new_invitation_id();
1303 let now = shared::iso_now();
1304 let expiration = (chrono::Utc::now() + chrono::Duration::days(7))
1305 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
1306 .to_string();
1307 json!({
1308 "InvitationId": id,
1309 "CreationDate": now,
1310 "ExpirationDate": expiration,
1311 "Status": "PENDING",
1312 "NetworkSummary": network_summary(network),
1313 "Arn": shared::invitation_arn(region, principal, &id),
1314 })
1315}
1316
1317fn network_framework_attributes(
1320 framework: &str,
1321 network_id: &str,
1322 body: &Value,
1323 region: &str,
1324) -> Value {
1325 if framework == "ETHEREUM" {
1326 let chain_id = match body.get("FrameworkVersion").and_then(Value::as_str) {
1328 Some("1.0") | Some("mainnet") => "1",
1329 _ => "1",
1330 };
1331 json!({ "Ethereum": { "ChainId": chain_id } })
1332 } else {
1333 let edition = body
1334 .get("FrameworkConfiguration")
1335 .and_then(|f| f.get("Fabric"))
1336 .and_then(|f| f.get("Edition"))
1337 .and_then(Value::as_str)
1338 .unwrap_or("STARTER");
1339 json!({
1340 "Fabric": {
1341 "OrderingServiceEndpoint": shared::fabric_ordering_endpoint(network_id, region),
1342 "Edition": edition,
1343 }
1344 })
1345 }
1346}
1347
1348fn network_summary(n: &Value) -> Value {
1351 let mut s = Map::new();
1352 for k in [
1353 "Id",
1354 "Name",
1355 "Description",
1356 "Framework",
1357 "FrameworkVersion",
1358 "Status",
1359 "CreationDate",
1360 "Arn",
1361 ] {
1362 if let Some(v) = n.get(k) {
1363 s.insert(k.to_string(), v.clone());
1364 }
1365 }
1366 Value::Object(s)
1367}
1368
1369fn member_summary(m: &Value, account: &str, region: &str) -> Value {
1370 let id = m.get("Id").and_then(Value::as_str).unwrap_or("");
1371 let mut s = Map::new();
1372 for k in ["Id", "Name", "Description", "Status", "CreationDate"] {
1373 if let Some(v) = m.get(k) {
1374 s.insert(k.to_string(), v.clone());
1375 }
1376 }
1377 s.insert("IsOwned".into(), json!(true));
1378 s.insert("Arn".into(), json!(shared::member_arn(region, account, id)));
1379 Value::Object(s)
1380}
1381
1382fn node_summary(n: &Value) -> Value {
1383 let mut s = Map::new();
1384 for k in [
1385 "Id",
1386 "Status",
1387 "CreationDate",
1388 "AvailabilityZone",
1389 "InstanceType",
1390 "Arn",
1391 ] {
1392 if let Some(v) = n.get(k) {
1393 s.insert(k.to_string(), v.clone());
1394 }
1395 }
1396 Value::Object(s)
1397}
1398
1399fn proposal_summary(p: &Value) -> Value {
1400 let mut s = Map::new();
1401 for k in [
1402 "ProposalId",
1403 "Description",
1404 "ProposedByMemberId",
1405 "ProposedByMemberName",
1406 "Status",
1407 "CreationDate",
1408 "ExpirationDate",
1409 "Arn",
1410 ] {
1411 if let Some(v) = p.get(k) {
1412 s.insert(k.to_string(), v.clone());
1413 }
1414 }
1415 Value::Object(s)
1416}
1417
1418fn accessor_summary(a: &Value) -> Value {
1419 let mut s = Map::new();
1420 for k in ["Id", "Type", "Status", "CreationDate", "Arn", "NetworkType"] {
1421 if let Some(v) = a.get(k) {
1422 s.insert(k.to_string(), v.clone());
1423 }
1424 }
1425 Value::Object(s)
1426}
1427
1428fn threshold_policy(network: Option<&Value>) -> (i64, String) {
1433 let policy = network
1434 .and_then(|n| n.get("VotingPolicy"))
1435 .and_then(|v| v.get("ApprovalThresholdPolicy"));
1436 let threshold = policy
1437 .and_then(|p| p.get("ThresholdPercentage"))
1438 .and_then(Value::as_i64)
1439 .unwrap_or(50);
1440 let comparator = policy
1441 .and_then(|p| p.get("ThresholdComparator"))
1442 .and_then(Value::as_str)
1443 .unwrap_or("GREATER_THAN")
1444 .to_string();
1445 (threshold, comparator)
1446}
1447
1448fn proposal_duration_hours(network: Option<&Value>) -> i64 {
1450 network
1451 .and_then(|n| n.get("VotingPolicy"))
1452 .and_then(|v| v.get("ApprovalThresholdPolicy"))
1453 .and_then(|p| p.get("ProposalDurationInHours"))
1454 .and_then(Value::as_i64)
1455 .unwrap_or(24)
1456}
1457
1458fn eligible_member_count(data: &ManagedBlockchainData, network_id: &str) -> i64 {
1460 data.members
1461 .values()
1462 .filter(|m| {
1463 m.get("NetworkId").and_then(Value::as_str) == Some(network_id)
1464 && m.get("Status").and_then(Value::as_str) != Some("DELETED")
1465 })
1466 .count() as i64
1467}
1468
1469fn decide_proposal(
1473 yes: i64,
1474 outstanding: i64,
1475 total: i64,
1476 threshold: i64,
1477 comparator: &str,
1478) -> String {
1479 if total <= 0 {
1480 return "IN_PROGRESS".to_string();
1481 }
1482 let meets = |y: i64| {
1483 let pct = (y as f64) * 100.0 / (total as f64);
1484 match comparator {
1485 "GREATER_THAN_OR_EQUAL_TO" => pct >= threshold as f64,
1486 _ => pct > threshold as f64,
1487 }
1488 };
1489 if meets(yes) {
1490 "APPROVED".to_string()
1491 } else if !meets(yes + outstanding) {
1492 "REJECTED".to_string()
1493 } else {
1494 "IN_PROGRESS".to_string()
1495 }
1496}
1497
1498fn promote_creating(v: &mut Value) -> bool {
1501 if let Some(obj) = v.as_object_mut() {
1502 if obj.get("Status").and_then(Value::as_str) == Some("CREATING") {
1503 obj.insert("Status".into(), json!("AVAILABLE"));
1504 return true;
1505 }
1506 }
1507 false
1508}
1509
1510fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
1513 Ok(AwsResponse::json_value(StatusCode::OK, v))
1514}
1515
1516fn ok_empty() -> Result<AwsResponse, AwsServiceError> {
1517 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1518}
1519
1520fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
1521 if req.body.is_empty() {
1522 return Ok(json!({}));
1523 }
1524 serde_json::from_slice(&req.body)
1525 .map_err(|e| invalid_request(&format!("The request body is malformed: {e}")))
1526}
1527
1528fn invalid_request(msg: &str) -> AwsServiceError {
1529 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidRequestException", msg)
1530}
1531
1532fn not_found(msg: &str) -> AwsServiceError {
1533 AwsServiceError::aws_error(StatusCode::NOT_FOUND, "ResourceNotFoundException", msg)
1534}
1535
1536fn illegal_action(msg: &str) -> AwsServiceError {
1537 AwsServiceError::aws_error(StatusCode::CONFLICT, "IllegalActionException", msg)
1538}
1539
1540fn check_arn(arn: &str) -> Result<(), AwsServiceError> {
1543 if arn.is_empty() || !arn.starts_with("arn:") {
1544 return Err(invalid_request(
1545 "The request failed because it is missing a valid resource ARN.",
1546 ));
1547 }
1548 Ok(())
1549}
1550
1551fn str_or(body: &Value, key: &str, default: &str) -> String {
1552 body.get(key)
1553 .and_then(Value::as_str)
1554 .unwrap_or(default)
1555 .to_string()
1556}
1557
1558fn echo(out: &mut Map<String, Value>, body: &Value, keys: &[&str]) {
1561 for key in keys {
1562 if let Some(v) = body.get(*key) {
1563 if !v.is_null() {
1564 out.insert((*key).to_string(), v.clone());
1565 }
1566 }
1567 }
1568}
1569
1570fn opt_eq(v: &Value, key: &str, filter: Option<&str>) -> bool {
1572 match filter {
1573 None => true,
1574 Some(f) => v.get(key).and_then(Value::as_str) == Some(f),
1575 }
1576}
1577
1578fn summary_id(v: &Value) -> &str {
1579 v.get("Id").and_then(Value::as_str).unwrap_or("")
1580}
1581
1582fn store_tags(data: &mut ManagedBlockchainData, arn: &str, body: &Value) {
1584 if let Some(tags) = body.get("Tags").and_then(Value::as_object) {
1585 if tags.is_empty() {
1586 return;
1587 }
1588 let entry = data.tags.entry(arn.to_string()).or_default();
1589 for (k, v) in tags {
1590 if let Some(s) = v.as_str() {
1591 entry.insert(k.clone(), s.to_string());
1592 }
1593 }
1594 }
1595}
1596
1597fn with_tags(data: &ManagedBlockchainData, arn: &str, obj: &Value) -> Value {
1600 let mut out = obj.clone();
1601 let tags = data.tags.get(arn).cloned().unwrap_or_default();
1602 if let Some(o) = out.as_object_mut() {
1603 o.insert("Tags".into(), json!(tags));
1604 }
1605 out
1606}
1607
1608fn parse_query(raw: &str) -> Vec<(String, String)> {
1609 raw.split('&')
1610 .filter(|p| !p.is_empty())
1611 .map(|pair| {
1612 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
1613 (
1614 percent_decode_str(k).decode_utf8_lossy().into_owned(),
1615 percent_decode_str(v).decode_utf8_lossy().into_owned(),
1616 )
1617 })
1618 .collect()
1619}
1620
1621fn query_one<'a>(q: &'a [(String, String)], key: &str) -> Option<&'a str> {
1622 q.iter()
1623 .find(|(k, _)| k == key)
1624 .map(|(_, v)| v.as_str())
1625 .filter(|v| !v.is_empty())
1626}
1627
1628const ID_MAX_LEN: usize = 32;
1631const TOKEN_MAX_LEN: usize = 128;
1632
1633pub const NETWORK_STATUS: &[&str] = &[
1634 "CREATING",
1635 "AVAILABLE",
1636 "CREATE_FAILED",
1637 "DELETING",
1638 "DELETED",
1639];
1640pub const MEMBER_STATUS: &[&str] = &[
1641 "CREATING",
1642 "AVAILABLE",
1643 "CREATE_FAILED",
1644 "UPDATING",
1645 "DELETING",
1646 "DELETED",
1647 "INACCESSIBLE_ENCRYPTION_KEY",
1648];
1649pub const NODE_STATUS: &[&str] = &[
1650 "CREATING",
1651 "AVAILABLE",
1652 "UNHEALTHY",
1653 "CREATE_FAILED",
1654 "UPDATING",
1655 "DELETING",
1656 "DELETED",
1657 "FAILED",
1658 "INACCESSIBLE_ENCRYPTION_KEY",
1659];
1660
1661fn check_id(id: &str, field: &str) -> Result<(), AwsServiceError> {
1665 let n = id.chars().count();
1666 if !(1..=ID_MAX_LEN).contains(&n) {
1667 return Err(invalid_request(&format!(
1668 "'{field}' length must be between 1 and {ID_MAX_LEN}, got {n}."
1669 )));
1670 }
1671 if id.contains('{') || id.contains('}') {
1672 return Err(invalid_request(&format!(
1673 "'{field}' is not a valid resource id: {id}"
1674 )));
1675 }
1676 Ok(())
1677}
1678
1679fn validate_list_query(
1682 q: &[(String, String)],
1683 cap: i64,
1684 enums: &[(&str, &[&str])],
1685) -> Result<(), AwsServiceError> {
1686 if let Some(v) = query_one(q, "maxResults") {
1687 let n: i64 = v
1688 .parse()
1689 .map_err(|_| invalid_request("maxResults must be an integer."))?;
1690 if n < 1 || n > cap {
1691 return Err(invalid_request(&format!(
1692 "maxResults must be between 1 and {cap}, got {n}."
1693 )));
1694 }
1695 }
1696 if let Some(t) = query_one(q, "nextToken") {
1697 if t.chars().count() > TOKEN_MAX_LEN {
1698 return Err(invalid_request(&format!(
1699 "nextToken must be at most {TOKEN_MAX_LEN} characters."
1700 )));
1701 }
1702 }
1703 for (key, allowed) in enums {
1704 if let Some(v) = query_one(q, key) {
1705 if !allowed.contains(&v) {
1706 return Err(invalid_request(&format!(
1707 "'{key}' must be one of [{}], got '{v}'.",
1708 allowed.join(", ")
1709 )));
1710 }
1711 }
1712 }
1713 Ok(())
1714}
1715
1716fn paginate(
1720 items: Vec<Value>,
1721 q: &[(String, String)],
1722) -> Result<(Vec<Value>, Option<String>), AwsServiceError> {
1723 let max = query_one(q, "maxResults")
1724 .and_then(|v| v.parse::<usize>().ok())
1725 .unwrap_or(usize::MAX);
1726 let start = query_one(q, "nextToken")
1727 .and_then(|v| v.parse::<usize>().ok())
1728 .unwrap_or(0);
1729 let start = start.min(items.len());
1730 let end = start.saturating_add(max).min(items.len());
1731 let page: Vec<Value> = items.get(start..end).unwrap_or(&[]).to_vec();
1732 let next = if end < items.len() {
1733 Some(end.to_string())
1734 } else {
1735 None
1736 };
1737 Ok((page, next))
1738}
1739
1740#[cfg(test)]
1741mod tests {
1742 use super::*;
1743 use fakecloud_core::multi_account::MultiAccountState;
1744 use parking_lot::RwLock;
1745
1746 fn svc() -> ManagedBlockchainService {
1747 ManagedBlockchainService::new(Arc::new(RwLock::new(MultiAccountState::new(
1748 "000000000000",
1749 "us-east-1",
1750 "",
1751 ))))
1752 }
1753
1754 fn ctx() -> Ctx {
1755 Ctx {
1756 account: "000000000000".to_string(),
1757 region: "us-east-1".to_string(),
1758 }
1759 }
1760
1761 fn body_of(resp: &AwsResponse) -> Value {
1762 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
1763 }
1764
1765 fn expect_err(r: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
1766 match r {
1767 Ok(_) => panic!("expected an error response"),
1768 Err(e) => e,
1769 }
1770 }
1771
1772 fn fabric_network(s: &ManagedBlockchainService) -> (String, String) {
1773 let resp = s
1774 .create_network(
1775 &ctx(),
1776 &json!({
1777 "ClientRequestToken": "t",
1778 "Name": "net",
1779 "Framework": "HYPERLEDGER_FABRIC",
1780 "FrameworkVersion": "2.2",
1781 "FrameworkConfiguration": { "Fabric": { "Edition": "STARTER" } },
1782 "VotingPolicy": { "ApprovalThresholdPolicy": {
1783 "ThresholdPercentage": 50,
1784 "ProposalDurationInHours": 24,
1785 "ThresholdComparator": "GREATER_THAN"
1786 } },
1787 "MemberConfiguration": {
1788 "Name": "member1",
1789 "FrameworkConfiguration": { "Fabric": {
1790 "AdminUsername": "admin", "AdminPassword": "Password123!"
1791 } }
1792 },
1793 "Description": "desc"
1794 }),
1795 )
1796 .unwrap();
1797 let out = body_of(&resp);
1798 (
1799 out["NetworkId"].as_str().unwrap().to_string(),
1800 out["MemberId"].as_str().unwrap().to_string(),
1801 )
1802 }
1803
1804 #[test]
1805 fn create_network_atomically_creates_first_member() {
1806 let s = svc();
1807 let (net_id, member_id) = fabric_network(&s);
1808 assert!(net_id.starts_with("n-"));
1809 assert!(member_id.starts_with("m-"));
1810
1811 let g = body_of(&s.get_network(&ctx(), &net_id).unwrap());
1813 assert_eq!(g["Network"]["Name"], "net");
1814 assert_eq!(g["Network"]["Description"], "desc");
1815 assert_eq!(g["Network"]["Status"], "AVAILABLE");
1816 assert_eq!(
1817 g["Network"]["FrameworkAttributes"]["Fabric"]["Edition"],
1818 "STARTER"
1819 );
1820
1821 s.reconcile(&ctx().account);
1823 let m = body_of(&s.get_member(&ctx(), &net_id, &member_id).unwrap());
1824 assert_eq!(m["Member"]["Status"], "AVAILABLE");
1825 assert!(m["Member"]["FrameworkAttributes"]["Fabric"]["CaEndpoint"].is_string());
1826 }
1827
1828 #[test]
1829 fn node_lifecycle() {
1830 let s = svc();
1831 let (net_id, _member) = fabric_network(&s);
1832 let created = body_of(
1833 &s.create_node(
1834 &ctx(),
1835 &net_id,
1836 &json!({
1837 "ClientRequestToken": "t2",
1838 "NodeConfiguration": { "InstanceType": "bc.t3.small", "StateDB": "CouchDB" }
1839 }),
1840 )
1841 .unwrap(),
1842 );
1843 let node_id = created["NodeId"].as_str().unwrap().to_string();
1844 assert!(node_id.starts_with("nd-"));
1845 s.reconcile(&ctx().account);
1846 let g = body_of(&s.get_node(&ctx(), &net_id, &node_id).unwrap());
1847 assert_eq!(g["Node"]["Status"], "AVAILABLE");
1848 assert!(g["Node"]["FrameworkAttributes"]["Fabric"]["PeerEndpoint"].is_string());
1849 s.delete_node(&ctx(), &net_id, &node_id).unwrap();
1851 let g2 = body_of(&s.get_node(&ctx(), &net_id, &node_id).unwrap());
1852 assert_eq!(g2["Node"]["Status"], "DELETED");
1853 }
1854
1855 #[test]
1856 fn proposal_vote_approves_and_materialises_invitation() {
1857 let s = svc();
1858 let (net_id, member_id) = fabric_network(&s);
1859 let prop = body_of(
1861 &s.create_proposal(
1862 &ctx(),
1863 &net_id,
1864 &json!({
1865 "ClientRequestToken": "t3",
1866 "MemberId": member_id,
1867 "Actions": { "Invitations": [ { "Principal": "000000000000" } ] }
1868 }),
1869 )
1870 .unwrap(),
1871 );
1872 let proposal_id = prop["ProposalId"].as_str().unwrap().to_string();
1873 s.vote_on_proposal(
1875 &ctx(),
1876 &net_id,
1877 &proposal_id,
1878 &json!({ "VoterMemberId": member_id, "Vote": "YES" }),
1879 )
1880 .unwrap();
1881 let g = body_of(&s.get_proposal(&ctx(), &net_id, &proposal_id).unwrap());
1882 assert_eq!(g["Proposal"]["Status"], "APPROVED");
1883 assert_eq!(g["Proposal"]["YesVoteCount"], 1);
1884 let votes = body_of(
1886 &s.list_proposal_votes(&ctx(), &net_id, &proposal_id, &[])
1887 .unwrap(),
1888 );
1889 assert_eq!(votes["ProposalVotes"].as_array().unwrap().len(), 1);
1890 let invs = body_of(&s.list_invitations(&ctx(), &[]).unwrap());
1892 assert_eq!(invs["Invitations"].as_array().unwrap().len(), 1);
1893 assert_eq!(invs["Invitations"][0]["Status"], "PENDING");
1894 }
1895
1896 #[test]
1897 fn create_member_requires_pending_invitation_and_existing_network() {
1898 let s = svc();
1902 let (net_id, member_id) = fabric_network(&s);
1903 let prop = body_of(
1905 &s.create_proposal(
1906 &ctx(),
1907 &net_id,
1908 &json!({
1909 "ClientRequestToken": "tp",
1910 "MemberId": member_id,
1911 "Actions": { "Invitations": [ { "Principal": "000000000000" } ] }
1912 }),
1913 )
1914 .unwrap(),
1915 );
1916 let pid = prop["ProposalId"].as_str().unwrap().to_string();
1917 s.vote_on_proposal(
1918 &ctx(),
1919 &net_id,
1920 &pid,
1921 &json!({ "VoterMemberId": member_id, "Vote": "YES" }),
1922 )
1923 .unwrap();
1924 let invs = body_of(&s.list_invitations(&ctx(), &[]).unwrap());
1925 let inv_id = invs["Invitations"][0]["InvitationId"]
1926 .as_str()
1927 .unwrap()
1928 .to_string();
1929
1930 let member_body = json!({
1931 "InvitationId": inv_id,
1932 "MemberConfiguration": {
1933 "Name": "member2",
1934 "FrameworkConfiguration": { "Fabric": {
1935 "AdminUsername": "admin", "AdminPassword": "Password123!"
1936 } }
1937 }
1938 });
1939
1940 let created = body_of(&s.create_member(&ctx(), &net_id, &member_body).unwrap());
1942 assert!(created["MemberId"].as_str().unwrap().starts_with("m-"));
1943
1944 let err = expect_err(s.create_member(&ctx(), &net_id, &member_body));
1946 assert!(format!("{err:?}").contains("InvalidRequestException"));
1947
1948 let err2 = expect_err(s.create_member(&ctx(), "n-DOESNOTEXIST0001", &member_body));
1950 assert!(format!("{err2:?}").contains("ResourceNotFoundException"));
1951 let err3 = expect_err(s.create_node(
1952 &ctx(),
1953 "n-DOESNOTEXIST0001",
1954 &json!({
1955 "ClientRequestToken": "tn",
1956 "NodeConfiguration": { "InstanceType": "bc.t3.small", "StateDB": "CouchDB" }
1957 }),
1958 ));
1959 assert!(format!("{err3:?}").contains("ResourceNotFoundException"));
1960 }
1961
1962 #[test]
1963 fn duplicate_vote_is_illegal_action() {
1964 let s = svc();
1965 let (net_id, member_id) = fabric_network(&s);
1966 let prop = body_of(
1967 &s.create_proposal(
1968 &ctx(),
1969 &net_id,
1970 &json!({
1971 "ClientRequestToken": "t",
1972 "MemberId": member_id,
1973 "Actions": { "Removals": [ { "MemberId": "m-OTHER" } ] }
1974 }),
1975 )
1976 .unwrap(),
1977 );
1978 let pid = prop["ProposalId"].as_str().unwrap().to_string();
1979 s.vote_on_proposal(
1980 &ctx(),
1981 &net_id,
1982 &pid,
1983 &json!({ "VoterMemberId": member_id, "Vote": "YES" }),
1984 )
1985 .unwrap();
1986 let err = expect_err(s.vote_on_proposal(
1988 &ctx(),
1989 &net_id,
1990 &pid,
1991 &json!({ "VoterMemberId": member_id, "Vote": "NO" }),
1992 ));
1993 assert!(format!("{err:?}").contains("IllegalActionException"));
1994 }
1995
1996 #[test]
1997 fn accessor_lifecycle() {
1998 let s = svc();
1999 let created = body_of(
2000 &s.create_accessor(
2001 &ctx(),
2002 &json!({ "ClientRequestToken": "t", "AccessorType": "BILLING_TOKEN", "NetworkType": "ETHEREUM_MAINNET" }),
2003 )
2004 .unwrap(),
2005 );
2006 let id = created["AccessorId"].as_str().unwrap().to_string();
2007 assert!(created["BillingToken"].is_string());
2008 let g = body_of(&s.get_accessor(&ctx(), &id).unwrap());
2009 assert_eq!(g["Accessor"]["Status"], "AVAILABLE");
2010 assert_eq!(g["Accessor"]["NetworkType"], "ETHEREUM_MAINNET");
2011 s.delete_accessor(&ctx(), &id).unwrap();
2012 let g2 = body_of(&s.get_accessor(&ctx(), &id).unwrap());
2013 assert_eq!(g2["Accessor"]["Status"], "PENDING_DELETION");
2014 }
2015
2016 #[test]
2017 fn list_networks_filters_and_paginates() {
2018 let s = svc();
2019 fabric_network(&s);
2020 fabric_network(&s);
2021 let all = body_of(&s.list_networks(&ctx(), &[]).unwrap());
2022 assert_eq!(all["Networks"].as_array().unwrap().len(), 2);
2023 let page1 = body_of(
2025 &s.list_networks(&ctx(), &[("maxResults".into(), "1".into())])
2026 .unwrap(),
2027 );
2028 assert_eq!(page1["Networks"].as_array().unwrap().len(), 1);
2029 assert!(page1["NextToken"].is_string());
2030 let eth = body_of(
2032 &s.list_networks(&ctx(), &[("framework".into(), "ETHEREUM".into())])
2033 .unwrap(),
2034 );
2035 assert_eq!(eth["Networks"].as_array().unwrap().len(), 0);
2036 }
2037
2038 #[test]
2039 fn unknown_network_is_resource_not_found() {
2040 let s = svc();
2041 let err = expect_err(s.get_network(&ctx(), "n-NOPE"));
2042 assert!(format!("{err:?}").contains("ResourceNotFoundException"));
2043 }
2044
2045 #[test]
2046 fn tag_untag_round_trips() {
2047 let s = svc();
2048 let (net_id, _m) = fabric_network(&s);
2049 let arn = shared::network_arn(&net_id);
2050 s.tag_resource(&ctx(), &arn, &json!({ "Tags": { "k": "v" } }))
2051 .unwrap();
2052 let listed = body_of(&s.list_tags(&ctx(), &arn).unwrap());
2053 assert_eq!(listed["Tags"]["k"], "v");
2054 s.untag_resource(&ctx(), &arn, &[("tagKeys".into(), "k".into())])
2055 .unwrap();
2056 let after = body_of(&s.list_tags(&ctx(), &arn).unwrap());
2057 assert!(after["Tags"].as_object().unwrap().is_empty());
2058 }
2059}