1use std::error::Error;
9use std::fmt::{self, Display, Formatter};
10
11use serde::{Deserialize, Serialize};
12
13pub use lodestone_types::{CollectionId, CollectionSecurityGeneration, LogicalMillis};
14
15pub const CLUSTER_CONTROL_SESSION_FORMAT_VERSION: u16 = 2;
16pub const MAX_CLUSTER_CONTROL_SESSION_BYTES: usize = 1_024;
17pub const MAX_CLUSTER_CONTROL_CREDENTIAL_BYTES: usize = 512;
18pub const CLUSTER_CONTROL_CREDENTIAL_PREFIX: &[u8] = b"lcc1_";
19pub const CLUSTER_CONTROL_CREDENTIAL_ENCODED_BYTES: usize = 48;
20pub const CLUSTER_CONTROL_REQUEST_FORMAT_VERSION: u16 = 2;
21pub const CLUSTER_CONTROL_RESPONSE_FORMAT_VERSION: u16 = 2;
22pub const MAX_CLUSTER_CONTROL_FRAME_BYTES: usize = 2 * 1024 * 1024;
23pub const MAX_CLUSTER_CONTROL_REQUEST_ID_BYTES: usize = 128;
24pub const MAX_CLUSTER_CONTROL_ERROR_CODE_BYTES: usize = 96;
25pub const COLLECTION_RETIREMENT_REQUEST_FORMAT_VERSION: u16 = 1;
26pub const COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION: u16 = 1;
27pub const COLLECTION_RETIREMENT_RECEIPT_FORMAT_VERSION: u16 = 1;
28pub const MAX_COLLECTION_RETIREMENT_CURSOR_BYTES: usize = 8 * 1024;
29pub const MAX_COLLECTION_RETIREMENT_BLOCKERS: usize = 32;
30
31const COLLECTION_RETIREMENT_REQUEST_DIGEST_DOMAIN: &[u8] =
32 b"lodestone/cluster-control/collection-retirement-request/v1\0";
33const COLLECTION_RETIREMENT_CLOSURE_DIGEST_DOMAIN: &[u8] =
34 b"lodestone/cluster-control/collection-retirement-closure/v1\0";
35
36#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
40#[serde(rename_all = "snake_case")]
41pub enum CollectionRetirementClosurePolicyV1 {
42 RetireOwnedChildren,
43}
44
45#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
46#[serde(deny_unknown_fields)]
47pub struct CollectionRetirementRequestV1 {
48 pub format_version: u16,
49 pub operation_id: [u8; 32],
50 pub collection_id: CollectionId,
51 pub expected_security_generation: CollectionSecurityGeneration,
52 pub closure_policy: CollectionRetirementClosurePolicyV1,
53}
54
55impl CollectionRetirementRequestV1 {
56 pub fn validate(&self) -> Result<(), CollectionRetirementContractError> {
57 if self.format_version != COLLECTION_RETIREMENT_REQUEST_FORMAT_VERSION
58 || self.operation_id == [0; 32]
59 || self.expected_security_generation.get() == 0
60 {
61 return Err(CollectionRetirementContractError::InvalidRequest);
62 }
63 Ok(())
64 }
65
66 pub fn canonical_request_digest(&self) -> Result<[u8; 32], CollectionRetirementContractError> {
67 self.validate()?;
68 canonical_collection_retirement_digest(COLLECTION_RETIREMENT_REQUEST_DIGEST_DOMAIN, self)
69 }
70}
71
72#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
73#[serde(rename_all = "snake_case")]
74pub enum CollectionRetirementOwnerV1 {
75 Admission,
76 Administration,
77 Index,
78 Security,
79 Operation,
80 Portability,
81 Retention,
82 Protection,
83 Usage,
84 Task,
85 ResidentRoute,
86}
87
88#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
89#[serde(rename_all = "snake_case")]
90pub enum CollectionRetirementBlockerKindV1 {
91 OwnerBusy,
92 ActiveAuthority,
93 PolicyRefusal,
94 RetainedEvidence,
95 PendingOperation,
96}
97
98#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
99#[serde(deny_unknown_fields)]
100pub struct CollectionRetirementBlockerV1 {
101 pub owner: CollectionRetirementOwnerV1,
102 pub kind: CollectionRetirementBlockerKindV1,
103}
104
105#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
106#[serde(deny_unknown_fields)]
107pub struct CollectionRetirementCursorV1 {
108 pub owner: CollectionRetirementOwnerV1,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub after: Option<Vec<u8>>,
111}
112
113impl CollectionRetirementCursorV1 {
114 fn validate(&self) -> Result<(), CollectionRetirementContractError> {
115 if self
116 .after
117 .as_ref()
118 .is_some_and(|after| after.is_empty() || after.len() > MAX_COLLECTION_RETIREMENT_CURSOR_BYTES)
119 {
120 return Err(CollectionRetirementContractError::InvalidStatus);
121 }
122 Ok(())
123 }
124}
125
126#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
127#[serde(deny_unknown_fields)]
128pub struct CollectionRetirementChildSummaryV1 {
129 pub administration_resources: u64,
130 pub indexes: u64,
131 pub security_resources: u64,
132 pub operations: u64,
133}
134
135#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
136#[serde(deny_unknown_fields)]
137pub struct CollectionRetirementReceiptV1 {
138 pub format_version: u16,
139 pub operation_id: [u8; 32],
140 pub request_digest: [u8; 32],
141 pub collection_id: CollectionId,
142 pub admitted_security_generation: CollectionSecurityGeneration,
143 pub retired_security_generation: CollectionSecurityGeneration,
144 pub admitted_at: LogicalMillis,
145 pub retired_at: LogicalMillis,
146 pub final_root_digest: [u8; 32],
147 pub closure_digest: [u8; 32],
148 pub child_summary: CollectionRetirementChildSummaryV1,
149}
150
151impl CollectionRetirementReceiptV1 {
152 pub fn validate(&self) -> Result<(), CollectionRetirementContractError> {
153 if self.format_version != COLLECTION_RETIREMENT_RECEIPT_FORMAT_VERSION
154 || self.operation_id == [0; 32]
155 || self.request_digest == [0; 32]
156 || self.admitted_security_generation.get() == 0
157 || self.retired_security_generation.get() <= self.admitted_security_generation.get()
158 || self.admitted_at.get() == 0
159 || self.retired_at < self.admitted_at
160 || self.final_root_digest == [0; 32]
161 || self.closure_digest == [0; 32]
162 {
163 return Err(CollectionRetirementContractError::InvalidReceipt);
164 }
165 if self.canonical_closure_digest()? != self.closure_digest {
166 return Err(CollectionRetirementContractError::InvalidReceipt);
167 }
168 Ok(())
169 }
170
171 pub fn canonical_closure_digest(&self) -> Result<[u8; 32], CollectionRetirementContractError> {
172 canonical_collection_retirement_digest(
173 COLLECTION_RETIREMENT_CLOSURE_DIGEST_DOMAIN,
174 &(
175 self.operation_id,
176 self.request_digest,
177 self.collection_id,
178 self.admitted_security_generation,
179 self.retired_security_generation,
180 self.admitted_at,
181 self.retired_at,
182 self.final_root_digest,
183 self.child_summary,
184 ),
185 )
186 }
187}
188
189#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
190#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
191pub enum CollectionRetirementOutcomeV1 {
192 NotSubmitted {
193 format_version: u16,
194 request_digest: [u8; 32],
195 },
196 Blocked {
197 format_version: u16,
198 request_digest: [u8; 32],
199 admitted_security_generation: CollectionSecurityGeneration,
200 cursor: CollectionRetirementCursorV1,
201 child_summary: CollectionRetirementChildSummaryV1,
202 blockers: Vec<CollectionRetirementBlockerV1>,
203 },
204 InProgress {
205 format_version: u16,
206 request_digest: [u8; 32],
207 admitted_security_generation: CollectionSecurityGeneration,
208 cursor: CollectionRetirementCursorV1,
209 child_summary: CollectionRetirementChildSummaryV1,
210 },
211 Indeterminate {
215 format_version: u16,
216 request: Box<CollectionRetirementRequestV1>,
217 },
218 Resolved {
219 format_version: u16,
220 receipt: Box<CollectionRetirementReceiptV1>,
221 },
222 KnownRetired {
223 format_version: u16,
224 request_digest: [u8; 32],
225 collection_id: CollectionId,
226 retired_security_generation: CollectionSecurityGeneration,
227 retired_at: LogicalMillis,
228 },
229 ExpiredUnknown {
230 format_version: u16,
231 request_digest: [u8; 32],
232 collection_id: CollectionId,
233 operation_id: [u8; 32],
234 },
235}
236
237impl CollectionRetirementOutcomeV1 {
238 pub fn validate(&self) -> Result<(), CollectionRetirementContractError> {
239 match self {
240 Self::NotSubmitted {
241 format_version,
242 request_digest,
243 } if *format_version == COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION && *request_digest != [0; 32] => Ok(()),
244 Self::Blocked {
245 format_version,
246 request_digest,
247 admitted_security_generation,
248 cursor,
249 blockers,
250 ..
251 } if *format_version == COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION
252 && *request_digest != [0; 32]
253 && admitted_security_generation.get() > 0
254 && !blockers.is_empty()
255 && blockers.len() <= MAX_COLLECTION_RETIREMENT_BLOCKERS
256 && blockers.windows(2).all(|pair| pair[0] < pair[1]) =>
257 {
258 cursor.validate()
259 }
260 Self::InProgress {
261 format_version,
262 request_digest,
263 admitted_security_generation,
264 cursor,
265 ..
266 } if *format_version == COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION
267 && *request_digest != [0; 32]
268 && admitted_security_generation.get() > 0 =>
269 {
270 cursor.validate()
271 }
272 Self::Indeterminate {
273 format_version,
274 request,
275 } if *format_version == COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION => request.validate(),
276 Self::Resolved {
277 format_version,
278 receipt,
279 } if *format_version == COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION => receipt.validate(),
280 Self::KnownRetired {
281 format_version,
282 request_digest,
283 retired_security_generation,
284 retired_at,
285 ..
286 } if *format_version == COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION
287 && *request_digest != [0; 32]
288 && retired_security_generation.get() > 0
289 && retired_at.get() > 0 =>
290 {
291 Ok(())
292 }
293 Self::ExpiredUnknown {
294 format_version,
295 request_digest,
296 operation_id,
297 ..
298 } if *format_version == COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION
299 && *request_digest != [0; 32]
300 && *operation_id != [0; 32] =>
301 {
302 Ok(())
303 }
304 Self::NotSubmitted { .. }
305 | Self::Blocked { .. }
306 | Self::InProgress { .. }
307 | Self::Indeterminate { .. }
308 | Self::Resolved { .. }
309 | Self::KnownRetired { .. }
310 | Self::ExpiredUnknown { .. } => Err(CollectionRetirementContractError::InvalidStatus),
311 }
312 }
313}
314
315fn canonical_collection_retirement_digest(
316 domain: &[u8],
317 value: &impl Serialize,
318) -> Result<[u8; 32], CollectionRetirementContractError> {
319 let encoded = serde_json::to_vec(value).map_err(|_| CollectionRetirementContractError::Codec)?;
320 let mut hasher = blake3::Hasher::new();
321 hasher.update(domain);
322 hasher.update(&encoded);
323 Ok(*hasher.finalize().as_bytes())
324}
325
326#[derive(Clone, Copy, Debug, Eq, PartialEq)]
327pub enum CollectionRetirementContractError {
328 InvalidRequest,
329 InvalidStatus,
330 InvalidReceipt,
331 Codec,
332}
333
334impl Display for CollectionRetirementContractError {
335 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
336 formatter.write_str(match self {
337 Self::InvalidRequest => "Collection-retirement request is invalid",
338 Self::InvalidStatus => "Collection-retirement status is invalid",
339 Self::InvalidReceipt => "Collection-retirement receipt is invalid",
340 Self::Codec => "Collection-retirement canonical encoding failed",
341 })
342 }
343}
344
345impl Error for CollectionRetirementContractError {}
346
347#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
348#[serde(rename_all = "snake_case")]
349pub enum ClusterControlCredentialKindV1 {
350 CollectionProvisioner,
351 CapacityObserver,
352 RecoveryOperator,
353}
354
355#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
356#[serde(rename_all = "snake_case")]
357pub enum ClusterControlActionV2 {
358 InitializeCollectionAuthority,
359 ActivateCollectionAuthority,
360 ResolveCollectionAuthority,
361 InspectCollectionRetirement,
362 RetireCollectionAuthority,
363 ObserveClusterCapacity,
364 ObserveResourceDiagnostics,
365 ReserveClusterControlCredentialRotation,
366 ActivateClusterControlCredentialRotation,
367 RevokeClusterControlCredential,
368 ResolveClusterControlCredential,
369 ListClusterBackups,
370 GetClusterBackup,
371 CreateClusterBackup,
372 ResolveClusterBackup,
373 DeleteClusterBackup,
374 ResolveClusterBackupDeletion,
375 GetClusterRestoreReadiness,
376 PrepareOfflineRecovery,
377}
378
379impl ClusterControlCredentialKindV1 {
380 #[must_use]
381 pub const fn admits(self, action: ClusterControlActionV2) -> bool {
382 match self {
383 Self::CollectionProvisioner => matches!(
384 action,
385 ClusterControlActionV2::InitializeCollectionAuthority
386 | ClusterControlActionV2::ActivateCollectionAuthority
387 | ClusterControlActionV2::ResolveCollectionAuthority
388 | ClusterControlActionV2::InspectCollectionRetirement
389 | ClusterControlActionV2::RetireCollectionAuthority
390 | ClusterControlActionV2::ReserveClusterControlCredentialRotation
391 | ClusterControlActionV2::ActivateClusterControlCredentialRotation
392 | ClusterControlActionV2::RevokeClusterControlCredential
393 | ClusterControlActionV2::ResolveClusterControlCredential
394 ),
395 Self::CapacityObserver => matches!(
396 action,
397 ClusterControlActionV2::ObserveClusterCapacity | ClusterControlActionV2::ObserveResourceDiagnostics
398 ),
399 Self::RecoveryOperator => matches!(
400 action,
401 ClusterControlActionV2::ReserveClusterControlCredentialRotation
402 | ClusterControlActionV2::ActivateClusterControlCredentialRotation
403 | ClusterControlActionV2::RevokeClusterControlCredential
404 | ClusterControlActionV2::ResolveClusterControlCredential
405 | ClusterControlActionV2::ListClusterBackups
406 | ClusterControlActionV2::GetClusterBackup
407 | ClusterControlActionV2::CreateClusterBackup
408 | ClusterControlActionV2::ResolveClusterBackup
409 | ClusterControlActionV2::DeleteClusterBackup
410 | ClusterControlActionV2::ResolveClusterBackupDeletion
411 | ClusterControlActionV2::GetClusterRestoreReadiness
412 | ClusterControlActionV2::PrepareOfflineRecovery
413 ),
414 }
415 }
416}
417
418#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
419#[serde(deny_unknown_fields)]
420pub struct ClusterControlSessionV2 {
421 pub format_version: u16,
422 pub credential_kind: ClusterControlCredentialKindV1,
423 pub credential: Vec<u8>,
424 pub request_id: String,
425 pub action: ClusterControlActionV2,
426 pub request_correlation_id: [u8; 16],
427}
428
429impl fmt::Debug for ClusterControlSessionV2 {
430 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
431 formatter
432 .debug_struct("ClusterControlSessionV2")
433 .field("format_version", &self.format_version)
434 .field("credential_kind", &self.credential_kind)
435 .field("credential", &"[REDACTED]")
436 .field("request_id", &self.request_id)
437 .field("action", &self.action)
438 .field("request_correlation_id", &self.request_correlation_id)
439 .finish()
440 }
441}
442
443impl ClusterControlSessionV2 {
444 pub fn validate(&self) -> Result<(), ClusterControlWireError> {
445 if self.format_version != CLUSTER_CONTROL_SESSION_FORMAT_VERSION {
446 return Err(ClusterControlWireError::UnsupportedVersion(self.format_version));
447 }
448 validate_cluster_control_credential_v1(&self.credential)?;
449 if self.request_id.is_empty()
450 || self.request_id.len() > MAX_CLUSTER_CONTROL_REQUEST_ID_BYTES
451 || self.request_id.bytes().any(|byte| byte.is_ascii_control())
452 {
453 return Err(ClusterControlWireError::InvalidRequestIdentity);
454 }
455 if !self.credential_kind.admits(self.action) {
456 return Err(ClusterControlWireError::ActionDenied);
457 }
458 Ok(())
459 }
460}
461
462pub fn validate_cluster_control_credential_v1(credential: &[u8]) -> Result<(), ClusterControlWireError> {
463 if credential.len() != CLUSTER_CONTROL_CREDENTIAL_ENCODED_BYTES
464 || !credential.starts_with(CLUSTER_CONTROL_CREDENTIAL_PREFIX)
465 || !credential[CLUSTER_CONTROL_CREDENTIAL_PREFIX.len()..]
466 .iter()
467 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
468 {
469 return Err(ClusterControlWireError::InvalidCredentialShape);
470 }
471 Ok(())
472}
473
474#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
475#[serde(deny_unknown_fields)]
476pub struct ClusterControlRequestV2 {
477 pub format_version: u16,
478 pub session: ClusterControlSessionV2,
479 pub body: serde_json::Value,
480}
481
482impl ClusterControlRequestV2 {
483 pub fn validate(&self) -> Result<(), ClusterControlWireError> {
484 if self.format_version != CLUSTER_CONTROL_REQUEST_FORMAT_VERSION {
485 return Err(ClusterControlWireError::UnsupportedVersion(self.format_version));
486 }
487 self.session.validate()?;
488 if !self.body.is_object() {
489 return Err(ClusterControlWireError::InvalidBody);
490 }
491 Ok(())
492 }
493}
494
495#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
496#[serde(rename_all = "snake_case")]
497pub enum ClusterControlResponseDispositionV2 {
498 RetrySameRequest,
499 ResolveOperation,
500 CorrectRequest,
501 Reauthenticate,
502 Terminal,
503}
504
505#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
506#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
507pub enum ClusterControlResponseOutcomeV2 {
508 Succeeded {
509 body: serde_json::Value,
510 },
511 Refused {
512 code: String,
513 disposition: ClusterControlResponseDispositionV2,
514 },
515}
516
517#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
518#[serde(deny_unknown_fields)]
519pub struct ClusterControlResponseV2 {
520 pub format_version: u16,
521 pub request_id: String,
522 pub request_correlation_id: [u8; 16],
523 pub outcome: ClusterControlResponseOutcomeV2,
524}
525
526impl ClusterControlResponseV2 {
527 pub fn validate(&self) -> Result<(), ClusterControlWireError> {
528 if self.format_version != CLUSTER_CONTROL_RESPONSE_FORMAT_VERSION {
529 return Err(ClusterControlWireError::UnsupportedVersion(self.format_version));
530 }
531 validate_request_id(&self.request_id)?;
532 match &self.outcome {
533 ClusterControlResponseOutcomeV2::Succeeded { body } if body.is_object() => Ok(()),
534 ClusterControlResponseOutcomeV2::Succeeded { .. } => Err(ClusterControlWireError::InvalidBody),
535 ClusterControlResponseOutcomeV2::Refused { code, .. }
536 if !code.is_empty()
537 && code.len() <= MAX_CLUSTER_CONTROL_ERROR_CODE_BYTES
538 && code
539 .bytes()
540 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') =>
541 {
542 Ok(())
543 }
544 ClusterControlResponseOutcomeV2::Refused { .. } => Err(ClusterControlWireError::InvalidErrorCode),
545 }
546 }
547}
548
549pub fn encode_cluster_control_request_v2(
550 request: &ClusterControlRequestV2,
551) -> Result<Vec<u8>, ClusterControlWireError> {
552 request.validate()?;
553 encode_frame(request)
554}
555
556pub fn decode_cluster_control_request_v2(bytes: &[u8]) -> Result<ClusterControlRequestV2, ClusterControlWireError> {
557 let request: ClusterControlRequestV2 = decode_frame(bytes)?;
558 request.validate()?;
559 Ok(request)
560}
561
562pub fn encode_cluster_control_response_v2(
563 response: &ClusterControlResponseV2,
564) -> Result<Vec<u8>, ClusterControlWireError> {
565 response.validate()?;
566 encode_frame(response)
567}
568
569pub fn decode_cluster_control_response_v2(bytes: &[u8]) -> Result<ClusterControlResponseV2, ClusterControlWireError> {
570 let response: ClusterControlResponseV2 = decode_frame(bytes)?;
571 response.validate()?;
572 Ok(response)
573}
574
575fn encode_frame(value: &impl Serialize) -> Result<Vec<u8>, ClusterControlWireError> {
576 let bytes = serde_json::to_vec(value).map_err(|error| ClusterControlWireError::Codec(error.to_string()))?;
577 if bytes.is_empty() || bytes.len() > MAX_CLUSTER_CONTROL_FRAME_BYTES {
578 return Err(ClusterControlWireError::FrameTooLarge(bytes.len()));
579 }
580 Ok(bytes)
581}
582
583fn decode_frame<T: for<'de> Deserialize<'de> + Serialize>(bytes: &[u8]) -> Result<T, ClusterControlWireError> {
584 if bytes.is_empty() || bytes.len() > MAX_CLUSTER_CONTROL_FRAME_BYTES {
585 return Err(ClusterControlWireError::FrameTooLarge(bytes.len()));
586 }
587 let value = serde_json::from_slice(bytes).map_err(|error| ClusterControlWireError::Codec(error.to_string()))?;
588 if serde_json::to_vec(&value).map_err(|error| ClusterControlWireError::Codec(error.to_string()))? != bytes {
589 return Err(ClusterControlWireError::NonCanonical);
590 }
591 Ok(value)
592}
593
594fn validate_request_id(request_id: &str) -> Result<(), ClusterControlWireError> {
595 if request_id.is_empty()
596 || request_id.len() > MAX_CLUSTER_CONTROL_REQUEST_ID_BYTES
597 || request_id.bytes().any(|byte| byte.is_ascii_control())
598 {
599 return Err(ClusterControlWireError::InvalidRequestIdentity);
600 }
601 Ok(())
602}
603
604pub fn encode_cluster_control_session_v2(
605 session: &ClusterControlSessionV2,
606) -> Result<Vec<u8>, ClusterControlWireError> {
607 session.validate()?;
608 let bytes = serde_json::to_vec(session).map_err(|error| ClusterControlWireError::Codec(error.to_string()))?;
609 if bytes.len() > MAX_CLUSTER_CONTROL_SESSION_BYTES {
610 return Err(ClusterControlWireError::FrameTooLarge(bytes.len()));
611 }
612 Ok(bytes)
613}
614
615pub fn decode_cluster_control_session_v2(bytes: &[u8]) -> Result<ClusterControlSessionV2, ClusterControlWireError> {
616 if bytes.is_empty() || bytes.len() > MAX_CLUSTER_CONTROL_SESSION_BYTES {
617 return Err(ClusterControlWireError::FrameTooLarge(bytes.len()));
618 }
619 let session: ClusterControlSessionV2 =
620 serde_json::from_slice(bytes).map_err(|error| ClusterControlWireError::Codec(error.to_string()))?;
621 session.validate()?;
622 if serde_json::to_vec(&session).map_err(|error| ClusterControlWireError::Codec(error.to_string()))? != bytes {
623 return Err(ClusterControlWireError::NonCanonical);
624 }
625 Ok(session)
626}
627
628#[derive(Clone, Debug, Eq, PartialEq)]
629pub enum ClusterControlWireError {
630 UnsupportedVersion(u16),
631 InvalidCredentialShape,
632 InvalidRequestIdentity,
633 InvalidBody,
634 InvalidErrorCode,
635 ActionDenied,
636 FrameTooLarge(usize),
637 NonCanonical,
638 Codec(String),
639}
640
641impl Display for ClusterControlWireError {
642 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
643 match self {
644 Self::UnsupportedVersion(version) => {
645 write!(formatter, "unsupported cluster-control wire version {version}")
646 }
647 Self::InvalidCredentialShape => formatter.write_str("invalid cluster-control credential shape"),
648 Self::InvalidRequestIdentity => formatter.write_str("invalid cluster-control request identity"),
649 Self::InvalidBody => formatter.write_str("invalid cluster-control request or response body"),
650 Self::InvalidErrorCode => formatter.write_str("invalid cluster-control error code"),
651 Self::ActionDenied => formatter.write_str("cluster-control credential kind does not admit the action"),
652 Self::FrameTooLarge(size) => write!(formatter, "cluster-control frame size {size} exceeds the bound"),
653 Self::NonCanonical => formatter.write_str("cluster-control frame is not canonical"),
654 Self::Codec(error) => write!(formatter, "cluster-control frame codec failed: {error}"),
655 }
656 }
657}
658
659impl Error for ClusterControlWireError {}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 fn retirement_request() -> Result<CollectionRetirementRequestV1, Box<dyn Error>> {
666 Ok(CollectionRetirementRequestV1 {
667 format_version: COLLECTION_RETIREMENT_REQUEST_FORMAT_VERSION,
668 operation_id: [11; 32],
669 collection_id: CollectionId::new(7)?,
670 expected_security_generation: CollectionSecurityGeneration::new(4),
671 closure_policy: CollectionRetirementClosurePolicyV1::RetireOwnedChildren,
672 })
673 }
674
675 fn session(
676 credential_kind: ClusterControlCredentialKindV1,
677 action: ClusterControlActionV2,
678 ) -> ClusterControlSessionV2 {
679 ClusterControlSessionV2 {
680 format_version: CLUSTER_CONTROL_SESSION_FORMAT_VERSION,
681 credential_kind,
682 credential: b"lcc1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_vec(),
683 request_id: "capacity-sample-1".to_owned(),
684 action,
685 request_correlation_id: [7; 16],
686 }
687 }
688
689 #[test]
690 fn credential_namespaces_are_disjoint_and_canonical() -> Result<(), Box<dyn Error>> {
691 let observer = session(
692 ClusterControlCredentialKindV1::CapacityObserver,
693 ClusterControlActionV2::ObserveClusterCapacity,
694 );
695 let bytes = encode_cluster_control_session_v2(&observer)?;
696 assert_eq!(decode_cluster_control_session_v2(&bytes)?, observer);
697
698 let provisioner_backup = session(
699 ClusterControlCredentialKindV1::CollectionProvisioner,
700 ClusterControlActionV2::GetClusterBackup,
701 );
702 assert_eq!(
703 provisioner_backup.validate(),
704 Err(ClusterControlWireError::ActionDenied)
705 );
706
707 let observer_mutation = session(
708 ClusterControlCredentialKindV1::CapacityObserver,
709 ClusterControlActionV2::InitializeCollectionAuthority,
710 );
711 assert_eq!(observer_mutation.validate(), Err(ClusterControlWireError::ActionDenied));
712
713 let recovery_capacity = session(
714 ClusterControlCredentialKindV1::RecoveryOperator,
715 ClusterControlActionV2::ObserveClusterCapacity,
716 );
717 assert_eq!(recovery_capacity.validate(), Err(ClusterControlWireError::ActionDenied));
718
719 let recovery_delete = session(
720 ClusterControlCredentialKindV1::RecoveryOperator,
721 ClusterControlActionV2::DeleteClusterBackup,
722 );
723 assert!(recovery_delete.validate().is_ok());
724 let provisioner_delete = session(
725 ClusterControlCredentialKindV1::CollectionProvisioner,
726 ClusterControlActionV2::DeleteClusterBackup,
727 );
728 assert_eq!(
729 provisioner_delete.validate(),
730 Err(ClusterControlWireError::ActionDenied)
731 );
732
733 let mut public_shape = observer;
734 public_shape.credential = b"1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_vec();
735 assert_eq!(
736 public_shape.validate(),
737 Err(ClusterControlWireError::InvalidCredentialShape)
738 );
739 Ok(())
740 }
741
742 #[test]
743 fn decoder_refuses_predecessor_newer_unknown_and_malformed_frames() -> Result<(), Box<dyn Error>> {
744 let observer = session(
745 ClusterControlCredentialKindV1::CapacityObserver,
746 ClusterControlActionV2::ObserveClusterCapacity,
747 );
748 let predecessor = String::from_utf8(encode_cluster_control_session_v2(&observer)?)?.replacen(
749 &format!("\"format_version\":{CLUSTER_CONTROL_SESSION_FORMAT_VERSION}"),
750 &format!("\"format_version\":{}", CLUSTER_CONTROL_SESSION_FORMAT_VERSION - 1),
751 1,
752 );
753 assert_eq!(
754 decode_cluster_control_session_v2(predecessor.as_bytes()),
755 Err(ClusterControlWireError::UnsupportedVersion(1))
756 );
757 let mut newer = serde_json::to_value(&observer)?;
758 newer["format_version"] = serde_json::json!(CLUSTER_CONTROL_SESSION_FORMAT_VERSION + 1);
759 assert!(matches!(
760 decode_cluster_control_session_v2(&serde_json::to_vec(&newer)?),
761 Err(ClusterControlWireError::UnsupportedVersion(_))
762 ));
763
764 let mut unknown = serde_json::to_value(observer)?;
765 unknown["collection_id"] = serde_json::json!(1);
766 assert!(decode_cluster_control_session_v2(&serde_json::to_vec(&unknown)?).is_err());
767
768 for malformed in [
769 Vec::new(),
770 vec![0xff],
771 vec![b'x'; MAX_CLUSTER_CONTROL_SESSION_BYTES + 1],
772 ] {
773 assert!(decode_cluster_control_session_v2(&malformed).is_err());
774 }
775 Ok(())
776 }
777
778 #[test]
779 fn request_and_response_envelopes_are_strict_bounded_and_correlated() -> Result<(), Box<dyn Error>> {
780 let request = ClusterControlRequestV2 {
781 format_version: CLUSTER_CONTROL_REQUEST_FORMAT_VERSION,
782 session: session(
783 ClusterControlCredentialKindV1::CapacityObserver,
784 ClusterControlActionV2::ObserveClusterCapacity,
785 ),
786 body: serde_json::json!({}),
787 };
788 let encoded = encode_cluster_control_request_v2(&request)?;
789 assert_eq!(decode_cluster_control_request_v2(&encoded)?, request);
790
791 let predecessor_request = String::from_utf8(encoded.clone())?.replacen(
792 &format!("\"format_version\":{CLUSTER_CONTROL_REQUEST_FORMAT_VERSION}"),
793 &format!("\"format_version\":{}", CLUSTER_CONTROL_REQUEST_FORMAT_VERSION - 1),
794 1,
795 );
796 assert_eq!(
797 decode_cluster_control_request_v2(predecessor_request.as_bytes()),
798 Err(ClusterControlWireError::UnsupportedVersion(1))
799 );
800
801 let response = ClusterControlResponseV2 {
802 format_version: CLUSTER_CONTROL_RESPONSE_FORMAT_VERSION,
803 request_id: request.session.request_id.clone(),
804 request_correlation_id: request.session.request_correlation_id,
805 outcome: ClusterControlResponseOutcomeV2::Succeeded {
806 body: serde_json::json!({"observation_id": "sample-1"}),
807 },
808 };
809 let encoded = encode_cluster_control_response_v2(&response)?;
810 assert_eq!(decode_cluster_control_response_v2(&encoded)?, response);
811
812 let predecessor_response = String::from_utf8(encoded)?.replacen(
813 &format!("\"format_version\":{CLUSTER_CONTROL_RESPONSE_FORMAT_VERSION}"),
814 &format!("\"format_version\":{}", CLUSTER_CONTROL_RESPONSE_FORMAT_VERSION - 1),
815 1,
816 );
817 assert_eq!(
818 decode_cluster_control_response_v2(predecessor_response.as_bytes()),
819 Err(ClusterControlWireError::UnsupportedVersion(1))
820 );
821
822 let mut unknown = serde_json::to_value(&request)?;
823 unknown["unexpected"] = serde_json::json!(true);
824 assert!(decode_cluster_control_request_v2(&serde_json::to_vec(&unknown)?).is_err());
825 assert!(matches!(
826 decode_cluster_control_request_v2(&vec![b'x'; MAX_CLUSTER_CONTROL_FRAME_BYTES + 1]),
827 Err(ClusterControlWireError::FrameTooLarge(_))
828 ));
829 Ok(())
830 }
831
832 #[test]
833 fn collection_retirement_identity_binds_every_admitted_semantic() -> Result<(), Box<dyn Error>> {
834 let request = retirement_request()?;
835 let digest = request.canonical_request_digest()?;
836 let mut changed = request.clone();
837 changed.expected_security_generation = CollectionSecurityGeneration::new(5);
838 assert_ne!(changed.canonical_request_digest()?, digest);
839
840 let mut invalid = request;
841 invalid.operation_id = [0; 32];
842 assert_eq!(
843 invalid.validate(),
844 Err(CollectionRetirementContractError::InvalidRequest)
845 );
846 Ok(())
847 }
848
849 #[test]
850 fn collection_retirement_receipt_and_recovery_statuses_fail_closed() -> Result<(), Box<dyn Error>> {
851 let request = retirement_request()?;
852 let mut receipt = CollectionRetirementReceiptV1 {
853 format_version: COLLECTION_RETIREMENT_RECEIPT_FORMAT_VERSION,
854 operation_id: request.operation_id,
855 request_digest: request.canonical_request_digest()?,
856 collection_id: request.collection_id,
857 admitted_security_generation: CollectionSecurityGeneration::new(5),
858 retired_security_generation: CollectionSecurityGeneration::new(6),
859 admitted_at: LogicalMillis::new(12),
860 retired_at: LogicalMillis::new(18),
861 final_root_digest: [21; 32],
862 closure_digest: [0; 32],
863 child_summary: CollectionRetirementChildSummaryV1 {
864 administration_resources: 2,
865 indexes: 3,
866 security_resources: 4,
867 operations: 0,
868 },
869 };
870 receipt.closure_digest = receipt.canonical_closure_digest()?;
871 receipt.validate()?;
872 let closure_digest = receipt.closure_digest;
873 receipt.child_summary.indexes += 1;
874 assert_eq!(
875 receipt.validate(),
876 Err(CollectionRetirementContractError::InvalidReceipt)
877 );
878 assert_ne!(receipt.canonical_closure_digest()?, closure_digest);
879
880 let blocked = CollectionRetirementOutcomeV1::Blocked {
881 format_version: COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION,
882 request_digest: request.canonical_request_digest()?,
883 admitted_security_generation: CollectionSecurityGeneration::new(5),
884 cursor: CollectionRetirementCursorV1 {
885 owner: CollectionRetirementOwnerV1::Protection,
886 after: None,
887 },
888 child_summary: CollectionRetirementChildSummaryV1::default(),
889 blockers: vec![CollectionRetirementBlockerV1 {
890 owner: CollectionRetirementOwnerV1::Protection,
891 kind: CollectionRetirementBlockerKindV1::PolicyRefusal,
892 }],
893 };
894 blocked.validate()?;
895 let indeterminate = CollectionRetirementOutcomeV1::Indeterminate {
896 format_version: COLLECTION_RETIREMENT_STATUS_FORMAT_VERSION,
897 request: Box::new(request),
898 };
899 indeterminate.validate()?;
900 Ok(())
901 }
902}