1use hoike_core::ResponderState;
2use hoike_core::config::Config;
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Instant;
6use tokio::sync::Mutex;
7
8#[derive(Clone)]
9pub struct AppState {
10 pub responder: Arc<ResponderState>,
11 pub live_signers: Arc<std::sync::RwLock<HashMap<String, Arc<LiveSignerState>>>>,
12 pub signer: Option<Arc<SignerContext>>,
13 pub gossip: Option<Arc<hoike_gossip::GossipNode>>,
17 pub admin: Arc<AdminState>,
18}
19
20pub struct LiveSignerState {
21 pub signer: Mutex<p256::ecdsa::SigningKey>,
22 pub responder_key_bytes: Vec<u8>,
23 pub validity_secs: u64,
24 pub responder_cert_der: Option<Vec<u8>>,
25}
26
27pub struct SignerContext {
33 pub sources: Mutex<hoike_sign::PersistentSources>,
34}
35
36pub struct AdminState {
37 pub config: Config,
38 pub started_at: Instant,
39 pub sessions: Mutex<HashMap<String, Session>>,
40}
41
42#[derive(Clone)]
43pub struct Session {
44 pub operator_name: String,
45 pub role: OperatorRole,
46 pub expires_at: Instant,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum OperatorRole {
52 Administrator,
53 Operator,
54 Viewer,
55}
56
57impl OperatorRole {
58 pub fn rank(self) -> u8 {
59 match self {
60 Self::Viewer => 1,
61 Self::Operator => 2,
62 Self::Administrator => 3,
63 }
64 }
65
66 pub fn has_at_least(self, min: Self) -> bool {
67 self.rank() >= min.rank()
68 }
69}
70
71impl std::str::FromStr for OperatorRole {
72 type Err = String;
73 fn from_str(s: &str) -> Result<Self, Self::Err> {
74 match s {
75 "administrator" => Ok(Self::Administrator),
76 "operator" => Ok(Self::Operator),
77 "viewer" => Ok(Self::Viewer),
78 other => Err(format!("unknown role: {other}")),
79 }
80 }
81}
82
83impl std::fmt::Display for OperatorRole {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::Administrator => write!(f, "administrator"),
87 Self::Operator => write!(f, "operator"),
88 Self::Viewer => write!(f, "viewer"),
89 }
90 }
91}
92
93impl AppState {
94 pub fn new(responder: ResponderState, config: Config) -> Self {
95 AppState {
96 responder: Arc::new(responder),
97 live_signers: Arc::new(std::sync::RwLock::new(HashMap::new())),
98 signer: None,
99 gossip: None,
100 admin: Arc::new(AdminState {
101 config,
102 started_at: Instant::now(),
103 sessions: Mutex::new(HashMap::new()),
104 }),
105 }
106 }
107
108 pub fn with_live_signer(self, live: LiveSignerState) -> Self {
110 let labels: Vec<_> = self
111 .admin
112 .config
113 .ca
114 .iter()
115 .filter(|ca| ca.nonce_policy == "live")
116 .map(|ca| ca.label.clone())
117 .collect();
118 assert_eq!(
119 labels.len(),
120 1,
121 "with_live_signer requires exactly one live CA"
122 );
123 self.with_live_signer_for(&labels[0], live)
124 }
125
126 pub fn with_live_signer_for(self, label: &str, live: LiveSignerState) -> Self {
127 self.live_signers
128 .write()
129 .expect("live signer lock poisoned")
130 .insert(label.into(), Arc::new(live));
131 self
132 }
133
134 pub fn live_signer_for(&self, label: &str) -> Option<Arc<LiveSignerState>> {
135 self.live_signers.read().ok()?.get(label).cloned()
136 }
137
138 pub fn reload_live_signer(&self, ca: &hoike_core::config::CaConfig) -> Result<(), String> {
139 if ca.nonce_policy != "live" {
140 return Ok(());
141 }
142 let live = LiveSignerState::from_config(ca)?;
143 self.live_signers
144 .write()
145 .map_err(|e| e.to_string())?
146 .insert(ca.label.clone(), Arc::new(live));
147 Ok(())
148 }
149
150 pub fn with_signer_context(mut self, ctx: SignerContext) -> Self {
153 self.signer = Some(Arc::new(ctx));
154 self
155 }
156
157 pub fn with_gossip(mut self, gossip: Arc<hoike_gossip::GossipNode>) -> Self {
161 self.gossip = Some(gossip);
162 self
163 }
164}
165
166impl LiveSignerState {
167 pub fn from_config(ca: &hoike_core::config::CaConfig) -> Result<Self, String> {
168 let material = hoike_sign::live::load_live_material(ca)?;
169 Ok(Self {
170 signer: Mutex::new(material.key),
171 responder_key_bytes: material.responder_key_bytes,
172 validity_secs: ca.validity_secs,
173 responder_cert_der: material.responder_cert_der,
174 })
175 }
176}