klieo_ops/
approver_registry.rs1use crate::key_registry::{load_ed25519_yaml, KeyRegistryError, StaticEd25519KeyRegistry};
10use ed25519_dalek::VerifyingKey;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::Path;
14use thiserror::Error;
15
16#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
19pub struct ApproverId(pub String);
20
21impl std::fmt::Display for ApproverId {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 f.write_str(&self.0)
24 }
25}
26
27#[derive(Debug, Error)]
29#[non_exhaustive]
30pub enum ApproverRegistryError {
31 #[error(transparent)]
33 Load(#[from] KeyRegistryError),
34}
35
36pub trait ApproverRegistry: Send + Sync {
38 fn lookup(&self, approver: &ApproverId) -> Option<VerifyingKey>;
41}
42
43pub struct StaticApproverRegistry {
51 inner: StaticEd25519KeyRegistry<ApproverId>,
52}
53
54impl StaticApproverRegistry {
55 #[must_use]
57 pub fn from_map(keys: HashMap<ApproverId, VerifyingKey>) -> Self {
58 Self {
59 inner: StaticEd25519KeyRegistry::from_map(keys),
60 }
61 }
62
63 pub fn from_yaml(path: impl AsRef<Path>) -> Result<Self, ApproverRegistryError> {
65 let keys = load_ed25519_yaml(path, ApproverId)?;
66 Ok(Self {
67 inner: StaticEd25519KeyRegistry::from_map(keys),
68 })
69 }
70}
71
72impl ApproverRegistry for StaticApproverRegistry {
73 fn lookup(&self, approver: &ApproverId) -> Option<VerifyingKey> {
74 use crate::key_registry::Ed25519KeyRegistry as _;
75 self.inner.lookup(approver)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use ed25519_dalek::SigningKey;
83
84 fn deterministic_key(seed: u8) -> SigningKey {
85 SigningKey::from_bytes(&[seed; 32])
86 }
87
88 #[test]
89 fn round_trip_via_yaml() {
90 let sk = deterministic_key(1);
91 let pk = sk.verifying_key();
92 use base64::Engine as _;
93 let pk_b64 = base64::engine::general_purpose::STANDARD.encode(pk.to_bytes());
94
95 let yaml = format!("alice@insurer.de: \"{pk_b64}\"\n");
96 let path = std::env::temp_dir().join("klieo-test-approvers.yaml");
97 std::fs::write(&path, yaml).unwrap();
98
99 let reg = StaticApproverRegistry::from_yaml(&path).expect("load");
100 let resolved = reg
101 .lookup(&ApproverId("alice@insurer.de".into()))
102 .expect("found");
103 assert_eq!(resolved.to_bytes(), pk.to_bytes());
104 }
105
106 #[test]
107 fn missing_approver_returns_none() {
108 let reg = StaticApproverRegistry::from_map(HashMap::new());
109 assert!(reg.lookup(&ApproverId("ghost".into())).is_none());
110 }
111}