confium_coordinator/coordinator/
admin.rs1use crate::coordinator::session::SessionState;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "action", rename_all = "snake_case")]
9pub enum AdminRequest {
10 ForceExpireSession { session_id: String },
12 Drain,
14 ListSessions,
16 PurgeQuorum { quorum_id: String },
18 GetDiagnostics,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(tag = "status", rename_all = "snake_case")]
25pub enum AdminResponse {
26 Ok { message: String },
28 SessionExpired { session_id: String },
30 Draining { active_sessions: usize },
32 SessionList { sessions: Vec<SessionSummary> },
34 QuorumPurged { quorum_id: String, count: usize },
36 Diagnostics { report_json: String },
38 Error { message: String },
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SessionSummary {
45 pub session_id: String,
46 pub state: String,
47 pub threshold: u32,
48 pub commitments: usize,
49 pub shares: usize,
50}
51
52#[derive(Debug, thiserror::Error)]
54pub enum AdminError {
55 #[error("session not found: {0}")]
56 SessionNotFound(String),
57 #[error("operation not permitted")]
58 NotPermitted,
59}
60
61pub fn is_admin(token: &str) -> bool {
64 !token.is_empty() && token.starts_with("admin-")
65}
66
67pub fn execute_admin(
69 request: &AdminRequest,
70 session_ids: &[String],
71 session_states: &dyn Fn(&str) -> Option<SessionState>,
72 session_threshold: &dyn Fn(&str) -> Option<u32>,
73 session_commitments: &dyn Fn(&str) -> Option<usize>,
74 session_shares: &dyn Fn(&str) -> Option<usize>,
75) -> AdminResponse {
76 match request {
77 AdminRequest::ForceExpireSession { session_id } => {
78 if !session_ids.contains(session_id) {
79 return AdminResponse::Error {
80 message: format!("session not found: {session_id}"),
81 };
82 }
83 AdminResponse::SessionExpired {
84 session_id: session_id.clone(),
85 }
86 }
87 AdminRequest::Drain => {
88 let active = session_ids
89 .iter()
90 .filter(|sid| session_states(sid) == Some(SessionState::Pending))
91 .count();
92 AdminResponse::Draining {
93 active_sessions: active,
94 }
95 }
96 AdminRequest::ListSessions => {
97 let sessions: Vec<SessionSummary> = session_ids
98 .iter()
99 .filter_map(|sid| {
100 let state = session_states(sid)?;
101 let threshold = session_threshold(sid).unwrap_or(0);
102 let commitments = session_commitments(sid).unwrap_or(0);
103 let shares = session_shares(sid).unwrap_or(0);
104 Some(SessionSummary {
105 session_id: sid.clone(),
106 state: format!("{state:?}"),
107 threshold,
108 commitments,
109 shares,
110 })
111 })
112 .collect();
113 AdminResponse::SessionList { sessions }
114 }
115 AdminRequest::PurgeQuorum { quorum_id: _ } => AdminResponse::QuorumPurged {
116 quorum_id: "placeholder".into(),
117 count: 0,
118 },
119 AdminRequest::GetDiagnostics => AdminResponse::Diagnostics {
120 report_json: "{}".into(),
121 },
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn admin_token_check() {
131 assert!(is_admin("admin-secret-123"));
132 assert!(!is_admin(""));
133 assert!(!is_admin("user-token"));
134 }
135
136 #[test]
137 fn force_expire_unknown_session_errors() {
138 let req = AdminRequest::ForceExpireSession {
139 session_id: "x".into(),
140 };
141 let resp = execute_admin(&req, &[], &|_| None, &|_| None, &|_| None, &|_| None);
142 match resp {
143 AdminResponse::Error { .. } => {}
144 _ => panic!("expected error"),
145 }
146 }
147
148 #[test]
149 fn force_expire_known_session() {
150 let req = AdminRequest::ForceExpireSession {
151 session_id: "s1".into(),
152 };
153 let resp = execute_admin(
154 &req,
155 &["s1".into()],
156 &|_| Some(SessionState::Pending),
157 &|_| Some(2),
158 &|_| Some(1),
159 &|_| Some(0),
160 );
161 match resp {
162 AdminResponse::SessionExpired { session_id } => assert_eq!(session_id, "s1"),
163 _ => panic!("expected SessionExpired"),
164 }
165 }
166
167 #[test]
168 fn drain_counts_active() {
169 let req = AdminRequest::Drain;
170 let ids = vec!["s1".into(), "s2".into(), "s3".into()];
171 let resp = execute_admin(
172 &req,
173 &ids,
174 &|sid| {
175 if sid == "s3" {
176 Some(SessionState::Completed)
177 } else {
178 Some(SessionState::Pending)
179 }
180 },
181 &|_| Some(2),
182 &|_| Some(0),
183 &|_| Some(0),
184 );
185 match resp {
186 AdminResponse::Draining { active_sessions } => assert_eq!(active_sessions, 2),
187 _ => panic!("expected Draining"),
188 }
189 }
190
191 #[test]
192 fn list_sessions_returns_summaries() {
193 let req = AdminRequest::ListSessions;
194 let ids = vec!["s1".into(), "s2".into()];
195 let resp = execute_admin(
196 &req,
197 &ids,
198 &|_| Some(SessionState::Pending),
199 &|_| Some(2),
200 &|sid| if sid == "s1" { Some(1) } else { Some(0) },
201 &|_| Some(0),
202 );
203 match resp {
204 AdminResponse::SessionList { sessions } => {
205 assert_eq!(sessions.len(), 2);
206 assert_eq!(sessions[0].commitments, 1);
207 }
208 _ => panic!("expected SessionList"),
209 }
210 }
211
212 #[test]
213 fn admin_request_serializes() {
214 let req = AdminRequest::ForceExpireSession {
215 session_id: "s1".into(),
216 };
217 let json = serde_json::to_string(&req).unwrap();
218 assert!(json.contains("force_expire_session"));
219 }
220
221 #[test]
222 fn admin_response_serializes() {
223 let resp = AdminResponse::Ok {
224 message: "done".into(),
225 };
226 let json = serde_json::to_string(&resp).unwrap();
227 assert!(json.contains("ok"));
228 }
229}