use std::error::Error;
use std::fmt::{self, Display, Formatter};
use lodestone_types::{ApiKeyId, AuthorizationBindingV1, CollectionId, PrincipalId, UnixMillis, WorkloadClassV1};
use serde::{Deserialize, Serialize};
use crate::{
Command, ErrorBody, RPC_MAX_CONTROL_PAYLOAD_BYTES, RequestBody, ResultBody, command_for_request, command_for_result,
};
pub const CORE_API_AUTHORITY_FORMAT_VERSION: u16 = 5;
pub const MAX_CORE_API_AUTHORITY_BYTES: usize = RPC_MAX_CONTROL_PAYLOAD_BYTES;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CoreApiAuthorityContextV2 {
pub collection_id: CollectionId,
pub principal_id: PrincipalId,
pub api_key_id: ApiKeyId,
pub authorization_binding: AuthorizationBindingV1,
pub workload_class: WorkloadClassV1,
pub request_correlation_id: [u8; 16],
pub response_diagnostic_id: [u8; 16],
pub absolute_deadline_unix_millis: UnixMillis,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CoreApiAuthorityRequestV2 {
pub format_version: u16,
pub context: CoreApiAuthorityContextV2,
pub request: RequestBody,
}
impl CoreApiAuthorityRequestV2 {
pub fn current(
context: CoreApiAuthorityContextV2,
request: RequestBody,
) -> Result<Self, CoreApiAuthorityWireError> {
let value = Self {
format_version: CORE_API_AUTHORITY_FORMAT_VERSION,
context,
request,
};
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), CoreApiAuthorityWireError> {
if self.format_version != CORE_API_AUTHORITY_FORMAT_VERSION {
return Err(CoreApiAuthorityWireError::UnsupportedVersion(self.format_version));
}
if self.context.request_correlation_id == [0; 16]
|| self.context.response_diagnostic_id == [0; 16]
|| self.context.absolute_deadline_unix_millis.get() == 0
|| !self.context.authorization_binding.is_valid()
|| self.context.collection_id != self.context.authorization_binding.collection_id
|| self.context.principal_id != self.context.authorization_binding.principal_id
|| self.context.api_key_id != self.context.authorization_binding.api_key_id
{
return Err(CoreApiAuthorityWireError::InvalidContext);
}
let command = command_for_request(&self.request);
if !core_api_authority_command_is_admitted(command) {
return Err(CoreApiAuthorityWireError::CommandNotAdmitted(command));
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "outcome", content = "body", rename_all = "snake_case", deny_unknown_fields)]
pub enum CoreApiAuthorityOutcomeV2 {
Success(Box<ResultBody>),
Error(ErrorBody),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CoreApiAuthorityResponseV2 {
pub format_version: u16,
pub outcome: CoreApiAuthorityOutcomeV2,
}
impl CoreApiAuthorityResponseV2 {
pub fn success(request: &CoreApiAuthorityRequestV2, result: ResultBody) -> Result<Self, CoreApiAuthorityWireError> {
if command_for_result(&result) != command_for_request(&request.request) {
return Err(CoreApiAuthorityWireError::SubstitutedResult);
}
Ok(Self {
format_version: CORE_API_AUTHORITY_FORMAT_VERSION,
outcome: CoreApiAuthorityOutcomeV2::Success(Box::new(result)),
})
}
#[must_use]
pub fn error(error: ErrorBody) -> Self {
Self {
format_version: CORE_API_AUTHORITY_FORMAT_VERSION,
outcome: CoreApiAuthorityOutcomeV2::Error(error),
}
}
pub fn validate_for(&self, request: &CoreApiAuthorityRequestV2) -> Result<(), CoreApiAuthorityWireError> {
if self.format_version != CORE_API_AUTHORITY_FORMAT_VERSION {
return Err(CoreApiAuthorityWireError::UnsupportedVersion(self.format_version));
}
if let CoreApiAuthorityOutcomeV2::Success(result) = &self.outcome
&& command_for_result(result) != command_for_request(&request.request)
{
return Err(CoreApiAuthorityWireError::SubstitutedResult);
}
Ok(())
}
}
pub fn encode_core_api_authority_request(
request: &CoreApiAuthorityRequestV2,
) -> Result<Vec<u8>, CoreApiAuthorityWireError> {
request.validate()?;
encode_canonical(request)
}
pub fn decode_core_api_authority_request(bytes: &[u8]) -> Result<CoreApiAuthorityRequestV2, CoreApiAuthorityWireError> {
let request: CoreApiAuthorityRequestV2 = decode_bounded(bytes)?;
request.validate()?;
require_canonical(&request, bytes)?;
Ok(request)
}
pub fn encode_core_api_authority_response(
request: &CoreApiAuthorityRequestV2,
response: &CoreApiAuthorityResponseV2,
) -> Result<Vec<u8>, CoreApiAuthorityWireError> {
response.validate_for(request)?;
encode_canonical(response)
}
pub fn decode_core_api_authority_response(
request: &CoreApiAuthorityRequestV2,
bytes: &[u8],
) -> Result<CoreApiAuthorityResponseV2, CoreApiAuthorityWireError> {
let response: CoreApiAuthorityResponseV2 = decode_bounded(bytes)?;
response.validate_for(request)?;
require_canonical(&response, bytes)?;
Ok(response)
}
fn encode_canonical<T: Serialize>(value: &T) -> Result<Vec<u8>, CoreApiAuthorityWireError> {
let bytes = serde_json::to_vec(value).map_err(|_| CoreApiAuthorityWireError::Codec)?;
if bytes.len() > MAX_CORE_API_AUTHORITY_BYTES {
return Err(CoreApiAuthorityWireError::TooLarge(bytes.len()));
}
Ok(bytes)
}
fn decode_bounded<T>(bytes: &[u8]) -> Result<T, CoreApiAuthorityWireError>
where
T: for<'de> Deserialize<'de>,
{
if bytes.is_empty() || bytes.len() > MAX_CORE_API_AUTHORITY_BYTES {
return Err(CoreApiAuthorityWireError::TooLarge(bytes.len()));
}
serde_json::from_slice(bytes).map_err(|_| CoreApiAuthorityWireError::Codec)
}
fn require_canonical<T: Serialize>(value: &T, bytes: &[u8]) -> Result<(), CoreApiAuthorityWireError> {
let canonical = serde_json::to_vec(&value).map_err(|_| CoreApiAuthorityWireError::Codec)?;
if canonical != bytes {
return Err(CoreApiAuthorityWireError::NonCanonical);
}
Ok(())
}
#[must_use]
pub const fn core_api_authority_command_is_admitted(command: Command) -> bool {
!matches!(
command,
Command::Discover
| Command::SubmitDocumentMutation
| Command::GetDocument
| Command::Search
| Command::OpenPit
| Command::RenewPit
| Command::ClosePit
)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CoreApiAuthorityWireError {
UnsupportedVersion(u16),
InvalidContext,
CommandNotAdmitted(Command),
SubstitutedResult,
TooLarge(usize),
Codec,
NonCanonical,
}
impl Display for CoreApiAuthorityWireError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
write!(formatter, "Core API authority wire refusal: {self:?}")
}
}
impl Error for CoreApiAuthorityWireError {}
#[cfg(test)]
mod tests {
use std::error::Error;
use lodestone_types::{
AUTHORIZATION_BINDING_FORMAT_VERSION, ApiKeyGeneration, ApiKeyId, CollectionId, CollectionSecurityGeneration,
PrincipalId, QuotaPolicyGeneration, RecoveryEra, SecurityPolicyRevision, SecurityPrincipalGeneration,
UnixMillis,
};
use super::*;
use crate::{GetQuotaRequest, GetQuotaResult, PrepareDocumentMutationRequest, RawJson, Resource, U64};
fn context() -> Result<CoreApiAuthorityContextV2, Box<dyn Error>> {
Ok(CoreApiAuthorityContextV2 {
collection_id: CollectionId::new(1)?,
principal_id: PrincipalId::new(2),
api_key_id: ApiKeyId::new(3),
authorization_binding: AuthorizationBindingV1 {
format_version: AUTHORIZATION_BINDING_FORMAT_VERSION,
recovery_era: RecoveryEra::new(1)?,
public_security_root_digest: [7; 32],
ordering_revision: SecurityPolicyRevision::new(4),
collection_id: CollectionId::new(1)?,
collection_security_generation: CollectionSecurityGeneration::new(5),
principal_id: PrincipalId::new(2),
principal_generation: SecurityPrincipalGeneration::new(6),
api_key_id: ApiKeyId::new(3),
api_key_generation: ApiKeyGeneration::new(7),
role_generations_digest: [8; 32],
quota_policy_generation: QuotaPolicyGeneration::new(9),
authorized_scope_digest: [10; 32],
},
workload_class: WorkloadClassV1::BatchAnalytics,
request_correlation_id: [5; 16],
response_diagnostic_id: [6; 16],
absolute_deadline_unix_millis: UnixMillis::new(6),
})
}
#[test]
fn control_request_and_exact_result_are_canonical() -> Result<(), Box<dyn Error>> {
let request = CoreApiAuthorityRequestV2::current(context()?, RequestBody::GetQuota(GetQuotaRequest {}))?;
let bytes = encode_core_api_authority_request(&request)?;
assert_eq!(decode_core_api_authority_request(&bytes)?, request);
let response = CoreApiAuthorityResponseV2::success(
&request,
ResultBody::GetQuota(GetQuotaResult {
revision: U64 { value: "4".to_owned() },
limits: Vec::new(),
usage: Vec::new(),
}),
)?;
let encoded = encode_core_api_authority_response(&request, &response)?;
assert_eq!(decode_core_api_authority_response(&request, &encoded)?, response);
Ok(())
}
#[test]
fn recoverable_prepare_newer_unknown_noncanonical_and_substitution_fail_closed() -> Result<(), Box<dyn Error>> {
let data = CoreApiAuthorityRequestV2::current(
context()?,
RequestBody::PrepareDocumentMutation(PrepareDocumentMutationRequest {
index: Resource {
kind: "index".to_owned(),
name: "logs".to_owned(),
incarnation: crate::Identity { value: "01".repeat(16) },
},
document_id: "1".to_owned(),
operation: "put".to_owned(),
source: Some(RawJson {
bytes_base64: "e30=".to_owned(),
}),
routing: None,
condition: None,
pipeline: None,
}),
);
assert!(data.is_ok());
assert!(!core_api_authority_command_is_admitted(Command::SubmitDocumentMutation));
let valid = CoreApiAuthorityRequestV2::current(context()?, RequestBody::GetQuota(GetQuotaRequest {}))?;
let mut newer = serde_json::to_value(&valid)?;
newer["format_version"] = serde_json::json!(CORE_API_AUTHORITY_FORMAT_VERSION + 1);
let newer = serde_json::to_vec(&newer)?;
assert_eq!(
decode_core_api_authority_request(&newer),
Err(CoreApiAuthorityWireError::UnsupportedVersion(
CORE_API_AUTHORITY_FORMAT_VERSION + 1
))
);
let with_unknown = serde_json::json!({
"format_version": CORE_API_AUTHORITY_FORMAT_VERSION,
"context": valid.context,
"request": valid.request,
"unknown": true
});
assert!(matches!(
decode_core_api_authority_request(&serde_json::to_vec(&with_unknown)?),
Err(CoreApiAuthorityWireError::Codec)
));
let mut noncanonical = encode_core_api_authority_request(&valid)?;
noncanonical.push(b' ');
assert!(matches!(
decode_core_api_authority_request(&noncanonical),
Err(CoreApiAuthorityWireError::NonCanonical)
));
assert!(matches!(
CoreApiAuthorityResponseV2::success(
&valid,
ResultBody::GetRuntimeStatus(crate::GetRuntimeStatusResult {
ready: true,
recovery_era: crate::Identity { value: "01".repeat(16) },
catalog_revision: U64 { value: "1".to_owned() },
policy_revision: U64 { value: "1".to_owned() },
redacted: crate::RuntimeStatusDetail {
metastore_revision: U64 { value: "1".to_owned() },
root_revision: U64 { value: "1".to_owned() },
logical_time: U64 { value: "1".to_owned() },
degraded_dependencies: Vec::new(),
},
})
),
Err(CoreApiAuthorityWireError::SubstitutedResult)
));
Ok(())
}
}