use std::error::Error;
use std::fmt::{self, Display, Formatter};
use serde::{Deserialize, Serialize};
pub const CLUSTER_CONTROL_SESSION_FORMAT_VERSION: u16 = 1;
pub const MAX_CLUSTER_CONTROL_SESSION_BYTES: usize = 1_024;
pub const MAX_CLUSTER_CONTROL_CREDENTIAL_BYTES: usize = 512;
pub const CLUSTER_CONTROL_CREDENTIAL_PREFIX: &[u8] = b"lcc1_";
pub const CLUSTER_CONTROL_CREDENTIAL_ENCODED_BYTES: usize = 48;
pub const CLUSTER_CONTROL_REQUEST_FORMAT_VERSION: u16 = 1;
pub const CLUSTER_CONTROL_RESPONSE_FORMAT_VERSION: u16 = 1;
pub const MAX_CLUSTER_CONTROL_FRAME_BYTES: usize = 2 * 1024 * 1024;
pub const MAX_CLUSTER_CONTROL_REQUEST_ID_BYTES: usize = 128;
pub const MAX_CLUSTER_CONTROL_ERROR_CODE_BYTES: usize = 96;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ClusterControlCredentialKindV1 {
CollectionProvisioner,
CapacityObserver,
RecoveryOperator,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ClusterControlActionV1 {
InitializeCollectionAuthority,
ActivateCollectionAuthority,
ResolveCollectionAuthority,
InspectCollectionRetirement,
RetireCollectionAuthority,
ObserveClusterCapacity,
ObserveResourceDiagnostics,
ReserveClusterControlCredentialRotation,
ActivateClusterControlCredentialRotation,
RevokeClusterControlCredential,
ResolveClusterControlCredential,
ListClusterBackups,
GetClusterBackup,
CreateClusterBackup,
ResolveClusterBackup,
GetClusterRestoreReadiness,
PrepareOfflineRecovery,
}
impl ClusterControlCredentialKindV1 {
#[must_use]
pub const fn admits(self, action: ClusterControlActionV1) -> bool {
match self {
Self::CollectionProvisioner => matches!(
action,
ClusterControlActionV1::InitializeCollectionAuthority
| ClusterControlActionV1::ActivateCollectionAuthority
| ClusterControlActionV1::ResolveCollectionAuthority
| ClusterControlActionV1::InspectCollectionRetirement
| ClusterControlActionV1::RetireCollectionAuthority
| ClusterControlActionV1::ReserveClusterControlCredentialRotation
| ClusterControlActionV1::ActivateClusterControlCredentialRotation
| ClusterControlActionV1::RevokeClusterControlCredential
| ClusterControlActionV1::ResolveClusterControlCredential
),
Self::CapacityObserver => matches!(
action,
ClusterControlActionV1::ObserveClusterCapacity | ClusterControlActionV1::ObserveResourceDiagnostics
),
Self::RecoveryOperator => matches!(
action,
ClusterControlActionV1::ReserveClusterControlCredentialRotation
| ClusterControlActionV1::ActivateClusterControlCredentialRotation
| ClusterControlActionV1::RevokeClusterControlCredential
| ClusterControlActionV1::ResolveClusterControlCredential
| ClusterControlActionV1::ListClusterBackups
| ClusterControlActionV1::GetClusterBackup
| ClusterControlActionV1::CreateClusterBackup
| ClusterControlActionV1::ResolveClusterBackup
| ClusterControlActionV1::GetClusterRestoreReadiness
| ClusterControlActionV1::PrepareOfflineRecovery
),
}
}
}
#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ClusterControlSessionV1 {
pub format_version: u16,
pub credential_kind: ClusterControlCredentialKindV1,
pub credential: Vec<u8>,
pub request_id: String,
pub action: ClusterControlActionV1,
pub request_correlation_id: [u8; 16],
}
impl fmt::Debug for ClusterControlSessionV1 {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ClusterControlSessionV1")
.field("format_version", &self.format_version)
.field("credential_kind", &self.credential_kind)
.field("credential", &"[REDACTED]")
.field("request_id", &self.request_id)
.field("action", &self.action)
.field("request_correlation_id", &self.request_correlation_id)
.finish()
}
}
impl ClusterControlSessionV1 {
pub fn validate(&self) -> Result<(), ClusterControlWireError> {
if self.format_version != CLUSTER_CONTROL_SESSION_FORMAT_VERSION {
return Err(ClusterControlWireError::UnsupportedVersion(self.format_version));
}
validate_cluster_control_credential_v1(&self.credential)?;
if self.request_id.is_empty()
|| self.request_id.len() > MAX_CLUSTER_CONTROL_REQUEST_ID_BYTES
|| self.request_id.bytes().any(|byte| byte.is_ascii_control())
{
return Err(ClusterControlWireError::InvalidRequestIdentity);
}
if !self.credential_kind.admits(self.action) {
return Err(ClusterControlWireError::ActionDenied);
}
Ok(())
}
}
pub fn validate_cluster_control_credential_v1(credential: &[u8]) -> Result<(), ClusterControlWireError> {
if credential.len() != CLUSTER_CONTROL_CREDENTIAL_ENCODED_BYTES
|| !credential.starts_with(CLUSTER_CONTROL_CREDENTIAL_PREFIX)
|| !credential[CLUSTER_CONTROL_CREDENTIAL_PREFIX.len()..]
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
return Err(ClusterControlWireError::InvalidCredentialShape);
}
Ok(())
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ClusterControlRequestV1 {
pub format_version: u16,
pub session: ClusterControlSessionV1,
pub body: serde_json::Value,
}
impl ClusterControlRequestV1 {
pub fn validate(&self) -> Result<(), ClusterControlWireError> {
if self.format_version != CLUSTER_CONTROL_REQUEST_FORMAT_VERSION {
return Err(ClusterControlWireError::UnsupportedVersion(self.format_version));
}
self.session.validate()?;
if !self.body.is_object() {
return Err(ClusterControlWireError::InvalidBody);
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ClusterControlResponseDispositionV1 {
RetrySameRequest,
ResolveOperation,
CorrectRequest,
Reauthenticate,
Terminal,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
pub enum ClusterControlResponseOutcomeV1 {
Succeeded {
body: serde_json::Value,
},
Refused {
code: String,
disposition: ClusterControlResponseDispositionV1,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ClusterControlResponseV1 {
pub format_version: u16,
pub request_id: String,
pub request_correlation_id: [u8; 16],
pub outcome: ClusterControlResponseOutcomeV1,
}
impl ClusterControlResponseV1 {
pub fn validate(&self) -> Result<(), ClusterControlWireError> {
if self.format_version != CLUSTER_CONTROL_RESPONSE_FORMAT_VERSION {
return Err(ClusterControlWireError::UnsupportedVersion(self.format_version));
}
validate_request_id(&self.request_id)?;
match &self.outcome {
ClusterControlResponseOutcomeV1::Succeeded { body } if body.is_object() => Ok(()),
ClusterControlResponseOutcomeV1::Succeeded { .. } => Err(ClusterControlWireError::InvalidBody),
ClusterControlResponseOutcomeV1::Refused { code, .. }
if !code.is_empty()
&& code.len() <= MAX_CLUSTER_CONTROL_ERROR_CODE_BYTES
&& code
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') =>
{
Ok(())
}
ClusterControlResponseOutcomeV1::Refused { .. } => Err(ClusterControlWireError::InvalidErrorCode),
}
}
}
pub fn encode_cluster_control_request_v1(
request: &ClusterControlRequestV1,
) -> Result<Vec<u8>, ClusterControlWireError> {
request.validate()?;
encode_frame(request)
}
pub fn decode_cluster_control_request_v1(bytes: &[u8]) -> Result<ClusterControlRequestV1, ClusterControlWireError> {
let request: ClusterControlRequestV1 = decode_frame(bytes)?;
request.validate()?;
Ok(request)
}
pub fn encode_cluster_control_response_v1(
response: &ClusterControlResponseV1,
) -> Result<Vec<u8>, ClusterControlWireError> {
response.validate()?;
encode_frame(response)
}
pub fn decode_cluster_control_response_v1(bytes: &[u8]) -> Result<ClusterControlResponseV1, ClusterControlWireError> {
let response: ClusterControlResponseV1 = decode_frame(bytes)?;
response.validate()?;
Ok(response)
}
fn encode_frame(value: &impl Serialize) -> Result<Vec<u8>, ClusterControlWireError> {
let bytes = serde_json::to_vec(value).map_err(|error| ClusterControlWireError::Codec(error.to_string()))?;
if bytes.is_empty() || bytes.len() > MAX_CLUSTER_CONTROL_FRAME_BYTES {
return Err(ClusterControlWireError::FrameTooLarge(bytes.len()));
}
Ok(bytes)
}
fn decode_frame<T: for<'de> Deserialize<'de> + Serialize>(bytes: &[u8]) -> Result<T, ClusterControlWireError> {
if bytes.is_empty() || bytes.len() > MAX_CLUSTER_CONTROL_FRAME_BYTES {
return Err(ClusterControlWireError::FrameTooLarge(bytes.len()));
}
let value = serde_json::from_slice(bytes).map_err(|error| ClusterControlWireError::Codec(error.to_string()))?;
if serde_json::to_vec(&value).map_err(|error| ClusterControlWireError::Codec(error.to_string()))? != bytes {
return Err(ClusterControlWireError::NonCanonical);
}
Ok(value)
}
fn validate_request_id(request_id: &str) -> Result<(), ClusterControlWireError> {
if request_id.is_empty()
|| request_id.len() > MAX_CLUSTER_CONTROL_REQUEST_ID_BYTES
|| request_id.bytes().any(|byte| byte.is_ascii_control())
{
return Err(ClusterControlWireError::InvalidRequestIdentity);
}
Ok(())
}
pub fn encode_cluster_control_session_v1(
session: &ClusterControlSessionV1,
) -> Result<Vec<u8>, ClusterControlWireError> {
session.validate()?;
let bytes = serde_json::to_vec(session).map_err(|error| ClusterControlWireError::Codec(error.to_string()))?;
if bytes.len() > MAX_CLUSTER_CONTROL_SESSION_BYTES {
return Err(ClusterControlWireError::FrameTooLarge(bytes.len()));
}
Ok(bytes)
}
pub fn decode_cluster_control_session_v1(bytes: &[u8]) -> Result<ClusterControlSessionV1, ClusterControlWireError> {
if bytes.is_empty() || bytes.len() > MAX_CLUSTER_CONTROL_SESSION_BYTES {
return Err(ClusterControlWireError::FrameTooLarge(bytes.len()));
}
let session: ClusterControlSessionV1 =
serde_json::from_slice(bytes).map_err(|error| ClusterControlWireError::Codec(error.to_string()))?;
session.validate()?;
if serde_json::to_vec(&session).map_err(|error| ClusterControlWireError::Codec(error.to_string()))? != bytes {
return Err(ClusterControlWireError::NonCanonical);
}
Ok(session)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ClusterControlWireError {
UnsupportedVersion(u16),
InvalidCredentialShape,
InvalidRequestIdentity,
InvalidBody,
InvalidErrorCode,
ActionDenied,
FrameTooLarge(usize),
NonCanonical,
Codec(String),
}
impl Display for ClusterControlWireError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedVersion(version) => {
write!(formatter, "unsupported cluster-control wire version {version}")
}
Self::InvalidCredentialShape => formatter.write_str("invalid cluster-control credential shape"),
Self::InvalidRequestIdentity => formatter.write_str("invalid cluster-control request identity"),
Self::InvalidBody => formatter.write_str("invalid cluster-control request or response body"),
Self::InvalidErrorCode => formatter.write_str("invalid cluster-control error code"),
Self::ActionDenied => formatter.write_str("cluster-control credential kind does not admit the action"),
Self::FrameTooLarge(size) => write!(formatter, "cluster-control frame size {size} exceeds the bound"),
Self::NonCanonical => formatter.write_str("cluster-control frame is not canonical"),
Self::Codec(error) => write!(formatter, "cluster-control frame codec failed: {error}"),
}
}
}
impl Error for ClusterControlWireError {}
#[cfg(test)]
mod tests {
use super::*;
fn session(
credential_kind: ClusterControlCredentialKindV1,
action: ClusterControlActionV1,
) -> ClusterControlSessionV1 {
ClusterControlSessionV1 {
format_version: CLUSTER_CONTROL_SESSION_FORMAT_VERSION,
credential_kind,
credential: b"lcc1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_vec(),
request_id: "capacity-sample-1".to_owned(),
action,
request_correlation_id: [7; 16],
}
}
#[test]
fn credential_namespaces_are_disjoint_and_canonical() -> Result<(), Box<dyn Error>> {
let observer = session(
ClusterControlCredentialKindV1::CapacityObserver,
ClusterControlActionV1::ObserveClusterCapacity,
);
let bytes = encode_cluster_control_session_v1(&observer)?;
assert_eq!(decode_cluster_control_session_v1(&bytes)?, observer);
let provisioner_backup = session(
ClusterControlCredentialKindV1::CollectionProvisioner,
ClusterControlActionV1::GetClusterBackup,
);
assert_eq!(
provisioner_backup.validate(),
Err(ClusterControlWireError::ActionDenied)
);
let observer_mutation = session(
ClusterControlCredentialKindV1::CapacityObserver,
ClusterControlActionV1::InitializeCollectionAuthority,
);
assert_eq!(observer_mutation.validate(), Err(ClusterControlWireError::ActionDenied));
let recovery_capacity = session(
ClusterControlCredentialKindV1::RecoveryOperator,
ClusterControlActionV1::ObserveClusterCapacity,
);
assert_eq!(recovery_capacity.validate(), Err(ClusterControlWireError::ActionDenied));
let mut public_shape = observer;
public_shape.credential = b"1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_vec();
assert_eq!(
public_shape.validate(),
Err(ClusterControlWireError::InvalidCredentialShape)
);
Ok(())
}
#[test]
fn decoder_refuses_newer_unknown_and_malformed_frames() -> Result<(), Box<dyn Error>> {
let observer = session(
ClusterControlCredentialKindV1::CapacityObserver,
ClusterControlActionV1::ObserveClusterCapacity,
);
let mut newer = serde_json::to_value(&observer)?;
newer["format_version"] = serde_json::json!(CLUSTER_CONTROL_SESSION_FORMAT_VERSION + 1);
assert!(matches!(
decode_cluster_control_session_v1(&serde_json::to_vec(&newer)?),
Err(ClusterControlWireError::UnsupportedVersion(_))
));
let mut unknown = serde_json::to_value(observer)?;
unknown["collection_id"] = serde_json::json!(1);
assert!(decode_cluster_control_session_v1(&serde_json::to_vec(&unknown)?).is_err());
for malformed in [
Vec::new(),
vec![0xff],
vec![b'x'; MAX_CLUSTER_CONTROL_SESSION_BYTES + 1],
] {
assert!(decode_cluster_control_session_v1(&malformed).is_err());
}
Ok(())
}
#[test]
fn request_and_response_envelopes_are_strict_bounded_and_correlated() -> Result<(), Box<dyn Error>> {
let request = ClusterControlRequestV1 {
format_version: CLUSTER_CONTROL_REQUEST_FORMAT_VERSION,
session: session(
ClusterControlCredentialKindV1::CapacityObserver,
ClusterControlActionV1::ObserveClusterCapacity,
),
body: serde_json::json!({}),
};
let encoded = encode_cluster_control_request_v1(&request)?;
assert_eq!(decode_cluster_control_request_v1(&encoded)?, request);
let response = ClusterControlResponseV1 {
format_version: CLUSTER_CONTROL_RESPONSE_FORMAT_VERSION,
request_id: request.session.request_id.clone(),
request_correlation_id: request.session.request_correlation_id,
outcome: ClusterControlResponseOutcomeV1::Succeeded {
body: serde_json::json!({"observation_id": "sample-1"}),
},
};
let encoded = encode_cluster_control_response_v1(&response)?;
assert_eq!(decode_cluster_control_response_v1(&encoded)?, response);
let mut unknown = serde_json::to_value(&request)?;
unknown["unexpected"] = serde_json::json!(true);
assert!(decode_cluster_control_request_v1(&serde_json::to_vec(&unknown)?).is_err());
assert!(matches!(
decode_cluster_control_request_v1(&vec![b'x'; MAX_CLUSTER_CONTROL_FRAME_BYTES + 1]),
Err(ClusterControlWireError::FrameTooLarge(_))
));
Ok(())
}
}