use std::fmt::Write as _;
use std::sync::{Arc, Mutex, PoisonError};
use liminal_protocol::wire::{ConversationId, ParticipantId};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OperatorCredentialReissueRequest {
pub conversation_id: ConversationId,
pub participant_id: ParticipantId,
pub expected_current_generation: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct OperatorCredentialReissued {
pub conversation_id: ConversationId,
pub participant_id: ParticipantId,
pub presented_generation: u64,
pub issued_generation: u64,
pub attach_secret: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "refusal", rename_all = "snake_case")]
pub enum OperatorCredentialReissueRefusal {
ConversationUnknown {
conversation_id: ConversationId,
},
ParticipantUnknown {
conversation_id: ConversationId,
participant_id: ParticipantId,
},
Retired {
conversation_id: ConversationId,
participant_id: ParticipantId,
retired_generation: u64,
},
LiveBinding {
conversation_id: ConversationId,
participant_id: ParticipantId,
current_generation: u64,
binding_state: &'static str,
},
DetachReplayOpen {
conversation_id: ConversationId,
participant_id: ParticipantId,
current_generation: u64,
},
LiveReceipt {
conversation_id: ConversationId,
participant_id: ParticipantId,
current_generation: u64,
receipt: &'static str,
},
GenerationMismatch {
conversation_id: ConversationId,
participant_id: ParticipantId,
presented_generation: u64,
current_generation: u64,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OperatorCredentialReissueOutcome {
Issued(OperatorCredentialReissued),
Refused(OperatorCredentialReissueRefusal),
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("operator credential re-issue could not be decided: {message}")]
pub struct OperatorCredentialReissueError {
pub message: String,
}
pub trait OperatorCredentialReissuer: std::fmt::Debug + Send + Sync {
fn reissue(
&self,
request: OperatorCredentialReissueRequest,
) -> Result<OperatorCredentialReissueOutcome, OperatorCredentialReissueError>;
}
#[derive(Clone, Debug, Default)]
pub struct SharedOperatorCredentialReissue {
reissuer: Arc<Mutex<Option<Arc<dyn OperatorCredentialReissuer>>>>,
}
impl SharedOperatorCredentialReissue {
pub fn install(&self, reissuer: Arc<dyn OperatorCredentialReissuer>) {
*self.reissuer.lock().unwrap_or_else(PoisonError::into_inner) = Some(reissuer);
}
#[must_use]
pub fn participant_installed(&self) -> bool {
self.reissuer
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_some()
}
pub fn reissue(
&self,
request: OperatorCredentialReissueRequest,
) -> Result<Option<OperatorCredentialReissueOutcome>, OperatorCredentialReissueError> {
let reissuer = self
.reissuer
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
reissuer
.map(|reissuer| reissuer.reissue(request))
.transpose()
}
}
#[must_use]
pub fn encode_hex(bytes: &[u8; 32]) -> String {
let mut rendered = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(rendered, "{byte:02x}");
}
rendered
}
#[cfg(test)]
mod tests {
use super::{
OperatorCredentialReissueError, OperatorCredentialReissueOutcome,
OperatorCredentialReissueRefusal, OperatorCredentialReissueRequest,
OperatorCredentialReissued, OperatorCredentialReissuer, SharedOperatorCredentialReissue,
encode_hex,
};
use std::sync::Arc;
#[derive(Debug)]
struct FixedReissuer(OperatorCredentialReissueOutcome);
impl OperatorCredentialReissuer for FixedReissuer {
fn reissue(
&self,
_request: OperatorCredentialReissueRequest,
) -> Result<OperatorCredentialReissueOutcome, OperatorCredentialReissueError> {
Ok(self.0.clone())
}
}
fn request() -> OperatorCredentialReissueRequest {
OperatorCredentialReissueRequest {
conversation_id: 7,
participant_id: 3,
expected_current_generation: 14,
}
}
#[test]
fn an_uninstalled_surface_reports_that_no_participant_is_installed() {
let surface = SharedOperatorCredentialReissue::default();
assert!(!surface.participant_installed());
assert_eq!(surface.reissue(request()), Ok(None));
}
#[test]
fn an_installed_authority_answers_the_surface() {
let issued = OperatorCredentialReissued {
conversation_id: 7,
participant_id: 3,
presented_generation: 14,
issued_generation: 15,
attach_secret: encode_hex(&[0xAB; 32]),
};
let surface = SharedOperatorCredentialReissue::default();
surface.install(Arc::new(FixedReissuer(
OperatorCredentialReissueOutcome::Issued(issued.clone()),
)));
assert!(surface.participant_installed());
assert_eq!(
surface.reissue(request()),
Ok(Some(OperatorCredentialReissueOutcome::Issued(issued)))
);
}
#[test]
fn the_generation_mismatch_refusal_serializes_both_generations() -> Result<(), serde_json::Error>
{
let refusal = OperatorCredentialReissueRefusal::GenerationMismatch {
conversation_id: 7,
participant_id: 3,
presented_generation: 14,
current_generation: 15,
};
let rendered = serde_json::to_value(&refusal)?;
assert_eq!(rendered["refusal"], "generation_mismatch");
assert_eq!(rendered["presented_generation"], 14);
assert_eq!(rendered["current_generation"], 15);
Ok(())
}
#[test]
fn hex_rendering_is_lowercase_and_fixed_width() {
let mut bytes = [0_u8; 32];
bytes[0] = 0x00;
bytes[1] = 0x0F;
bytes[31] = 0xFF;
let rendered = encode_hex(&bytes);
assert_eq!(rendered.len(), 64);
assert!(rendered.starts_with("000f"));
assert!(rendered.ends_with("ff"));
}
}