acton_htmx/handlers/
cedar_admin.rs1#[cfg(feature = "cedar")]
18use axum::{
19 extract::State,
20 http::StatusCode,
21 response::{IntoResponse, Response},
22 Json,
23};
24
25#[cfg(feature = "cedar")]
26use serde::{Deserialize, Serialize};
27
28#[cfg(feature = "cedar")]
29use crate::{
30 auth::{user::User, Authenticated},
31 middleware::cedar::CedarAuthz,
32};
33
34#[cfg(feature = "cedar")]
36#[derive(Debug, Serialize, Deserialize)]
37pub struct ReloadPolicyResponse {
38 pub success: bool,
40
41 pub message: String,
43
44 pub timestamp: String,
46}
47
48#[cfg(feature = "cedar")]
50#[derive(Debug, Serialize, Deserialize)]
51pub struct PolicyStatusResponse {
52 pub enabled: bool,
54
55 pub policy_path: String,
57
58 pub hot_reload: bool,
60
61 pub failure_mode: String,
63
64 pub cache_enabled: bool,
66}
67
68#[cfg(feature = "cedar")]
98pub async fn reload_policies(
99 State(cedar): State<CedarAuthz>,
100 Authenticated(user): Authenticated<User>,
101) -> Result<Response, StatusCode> {
102 if !user.roles.contains(&"admin".to_string()) {
104 tracing::warn!(
105 user_id = user.id,
106 email = %user.email,
107 "Non-admin user attempted to reload Cedar policies"
108 );
109 return Err(StatusCode::FORBIDDEN);
110 }
111
112 match cedar.reload_policies().await {
114 Ok(()) => {
115 let response = ReloadPolicyResponse {
116 success: true,
117 message: "Cedar policies reloaded successfully".to_string(),
118 timestamp: chrono::Utc::now().to_rfc3339(),
119 };
120
121 tracing::info!(
122 user_id = user.id,
123 email = %user.email,
124 "Cedar policies reloaded by admin"
125 );
126
127 Ok((StatusCode::OK, Json(response)).into_response())
128 }
129 Err(e) => {
130 tracing::error!(
131 error = ?e,
132 user_id = user.id,
133 "Failed to reload Cedar policies"
134 );
135
136 let response = ReloadPolicyResponse {
137 success: false,
138 message: format!("Failed to reload policies: {e}"),
139 timestamp: chrono::Utc::now().to_rfc3339(),
140 };
141
142 Ok((StatusCode::INTERNAL_SERVER_ERROR, Json(response)).into_response())
143 }
144 }
145}
146
147#[cfg(feature = "cedar")]
173pub async fn policy_status(
174 State(cedar): State<CedarAuthz>,
175 Authenticated(user): Authenticated<User>,
176) -> Result<Response, StatusCode> {
177 if !user.roles.contains(&"admin".to_string()) {
179 tracing::warn!(
180 user_id = user.id,
181 email = %user.email,
182 "Non-admin user attempted to view Cedar policy status"
183 );
184 return Err(StatusCode::FORBIDDEN);
185 }
186
187 let config = cedar.config();
189
190 let response = PolicyStatusResponse {
191 enabled: config.enabled,
192 policy_path: config.policy_path.display().to_string(),
193 hot_reload: config.hot_reload,
194 failure_mode: format!("{:?}", config.failure_mode).to_lowercase(),
195 cache_enabled: config.cache_enabled,
196 };
197
198 tracing::debug!(
199 user_id = user.id,
200 email = %user.email,
201 "Cedar policy status requested by admin"
202 );
203
204 Ok((StatusCode::OK, Json(response)).into_response())
205}
206
207#[cfg(test)]
208#[cfg(feature = "cedar")]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_reload_response_serialization() {
214 let response = ReloadPolicyResponse {
215 success: true,
216 message: "Policies reloaded".to_string(),
217 timestamp: "2025-11-22T10:30:00Z".to_string(),
218 };
219
220 let json = serde_json::to_string(&response).unwrap();
221 assert!(json.contains("\"success\":true"));
222 assert!(json.contains("Policies reloaded"));
223 }
224
225 #[test]
226 fn test_status_response_serialization() {
227 let response = PolicyStatusResponse {
228 enabled: true,
229 policy_path: "policies/app.cedar".to_string(),
230 hot_reload: false,
231 failure_mode: "closed".to_string(),
232 cache_enabled: true,
233 };
234
235 let json = serde_json::to_string(&response).unwrap();
236 assert!(json.contains("\"enabled\":true"));
237 assert!(json.contains("policies/app.cedar"));
238 }
239}