Skip to main content

reddb_server/cluster/
identity.rs

1//! Per-node cluster identities shared by replication and voting.
2
3use rustls::pki_types::CertificateDer;
4
5/// Stable identity for a node participating in cluster protocols.
6///
7/// The value is the X.509 subject distinguished name from a validated
8/// node certificate. Replication peers and cluster voters intentionally
9/// use this same type so acknowledgements and votes cannot drift into
10/// separate identity namespaces.
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct NodeIdentity(String);
13
14/// Replication and witness-voting identities are the same cluster node identity.
15pub type ReplicationPeerIdentity = NodeIdentity;
16pub type ClusterVoterIdentity = NodeIdentity;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum NodeIdentityError {
20    EmptySubject,
21    CertificateParse(String),
22}
23
24impl std::fmt::Display for NodeIdentityError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Self::EmptySubject => write!(f, "certificate subject is empty"),
28            Self::CertificateParse(err) => write!(f, "certificate parse error: {err}"),
29        }
30    }
31}
32
33impl std::error::Error for NodeIdentityError {}
34
35impl NodeIdentity {
36    pub fn from_certificate_subject(subject: impl AsRef<str>) -> Result<Self, NodeIdentityError> {
37        let subject = subject.as_ref().trim();
38        if subject.is_empty() {
39            return Err(NodeIdentityError::EmptySubject);
40        }
41        Ok(Self(subject.to_string()))
42    }
43
44    pub fn from_peer_certificate_der(cert: &CertificateDer<'_>) -> Result<Self, NodeIdentityError> {
45        let (_, parsed) = x509_parser::parse_x509_certificate(cert.as_ref())
46            .map_err(|err| NodeIdentityError::CertificateParse(format!("{err:?}")))?;
47        Self::from_certificate_subject(parsed.subject().to_string())
48    }
49
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53}
54
55impl std::fmt::Display for NodeIdentity {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.write_str(&self.0)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn node_identity_rejects_empty_certificate_subjects() {
67        assert_eq!(
68            NodeIdentity::from_certificate_subject("   ").unwrap_err(),
69            NodeIdentityError::EmptySubject
70        );
71    }
72
73    #[test]
74    fn cluster_voter_and_replication_peer_share_node_identity() {
75        let voter = ClusterVoterIdentity::from_certificate_subject("CN=node-a").unwrap();
76        let replica = ReplicationPeerIdentity::from_certificate_subject("CN=node-a").unwrap();
77
78        assert_eq!(voter, replica);
79        assert_eq!(voter.as_str(), "CN=node-a");
80    }
81}