1use actix_web::body::MessageBody;
4use actix_web::dev::{ServiceRequest, ServiceResponse};
5use actix_web::middleware::from_fn;
6use actix_web::{App, HttpMessage, HttpServer, web};
7
8use haystack_core::auth::{AuthHeader, parse_auth_header};
9use haystack_core::graph::SharedGraph;
10use haystack_core::ontology::DefNamespace;
11
12use crate::actions::ActionRegistry;
13use crate::auth::AuthManager;
14use crate::federation::Federation;
15use crate::his_store::HisStore;
16use crate::ops;
17use crate::state::AppState;
18use crate::ws;
19use crate::ws::WatchManager;
20
21pub struct HaystackServer {
23 graph: SharedGraph,
24 namespace: DefNamespace,
25 auth_manager: AuthManager,
26 actions: ActionRegistry,
27 federation: Federation,
28 port: u16,
29 host: String,
30}
31
32impl HaystackServer {
33 pub fn new(graph: SharedGraph) -> Self {
35 Self {
36 graph,
37 namespace: DefNamespace::new(),
38 auth_manager: AuthManager::empty(),
39 actions: ActionRegistry::new(),
40 federation: Federation::new(),
41 port: 8080,
42 host: "127.0.0.1".to_string(),
43 }
44 }
45
46 pub fn with_namespace(mut self, ns: DefNamespace) -> Self {
48 self.namespace = ns;
49 self
50 }
51
52 pub fn with_auth(mut self, auth: AuthManager) -> Self {
54 self.auth_manager = auth;
55 self
56 }
57
58 pub fn port(mut self, port: u16) -> Self {
60 self.port = port;
61 self
62 }
63
64 pub fn host(mut self, host: &str) -> Self {
66 self.host = host.to_string();
67 self
68 }
69
70 pub fn with_actions(mut self, actions: ActionRegistry) -> Self {
72 self.actions = actions;
73 self
74 }
75
76 pub fn with_federation(mut self, federation: Federation) -> Self {
78 self.federation = federation;
79 self
80 }
81
82 pub async fn run(self) -> std::io::Result<()> {
84 let state = web::Data::new(AppState {
85 graph: self.graph,
86 namespace: parking_lot::RwLock::new(self.namespace),
87 auth: self.auth_manager,
88 watches: WatchManager::new(),
89 actions: self.actions,
90 his: HisStore::new(),
91 started_at: std::time::Instant::now(),
92 federation: self.federation,
93 });
94
95 log::info!("Starting haystack-server on {}:{}", self.host, self.port);
96
97 HttpServer::new(move || {
98 App::new()
99 .app_data(state.clone())
100 .app_data(actix_web::web::PayloadConfig::default().limit(2 * 1024 * 1024))
101 .wrap(from_fn(auth_middleware))
102 .configure(ops::configure)
103 .route("/api/ws", web::get().to(ws::ws_handler))
104 })
105 .bind((self.host.as_str(), self.port))?
106 .run()
107 .await
108 }
109}
110
111fn required_permission(path: &str) -> Option<&'static str> {
116 if path.starts_with("/api/system/") {
118 return Some("admin");
119 }
120
121 match path {
123 "/api/pointWrite"
124 | "/api/hisWrite"
125 | "/api/invokeAction"
126 | "/api/loadLib"
127 | "/api/unloadLib"
128 | "/api/import"
129 | "/api/federation/sync" => return Some("write"),
130 _ => {}
131 }
132
133 Some("read")
138}
139
140async fn auth_middleware(
147 req: ServiceRequest,
148 next: actix_web::middleware::Next<impl MessageBody>,
149) -> Result<ServiceResponse<impl MessageBody>, actix_web::Error> {
150 let path = req.path().to_string();
151 let method = req.method().clone();
152
153 if path == "/api/about" {
155 return next.call(req).await;
156 }
157
158 if (path == "/api/ops" || path == "/api/formats") && method == actix_web::http::Method::GET {
160 return next.call(req).await;
161 }
162
163 let auth_enabled = {
165 let state = req
166 .app_data::<web::Data<AppState>>()
167 .expect("AppState must be configured");
168 state.auth.is_enabled()
169 };
170
171 if !auth_enabled {
172 return next.call(req).await;
174 }
175
176 let auth_header = req
178 .headers()
179 .get("Authorization")
180 .and_then(|v| v.to_str().ok())
181 .map(|s| s.to_string());
182
183 match auth_header {
184 Some(header) => {
185 match parse_auth_header(&header) {
186 Ok(AuthHeader::Bearer { auth_token }) => {
187 let user = {
188 let state = req
189 .app_data::<web::Data<AppState>>()
190 .expect("AppState must be configured");
191 state.auth.validate_token(&auth_token)
192 };
193
194 match user {
195 Some(auth_user) => {
196 if let Some(required) = required_permission(&path)
198 && !AuthManager::check_permission(&auth_user, required)
199 {
200 return Err(crate::error::HaystackError::forbidden(format!(
201 "user '{}' lacks '{}' permission",
202 auth_user.username, required
203 ))
204 .into());
205 }
206
207 req.extensions_mut().insert(auth_user);
209 next.call(req).await
210 }
211 None => Err(crate::error::HaystackError::new(
212 "invalid or expired auth token",
213 actix_web::http::StatusCode::UNAUTHORIZED,
214 )
215 .into()),
216 }
217 }
218 _ => Err(crate::error::HaystackError::new(
219 "BEARER token required",
220 actix_web::http::StatusCode::UNAUTHORIZED,
221 )
222 .into()),
223 }
224 }
225 None => Err(crate::error::HaystackError::new(
226 "Authorization header required",
227 actix_web::http::StatusCode::UNAUTHORIZED,
228 )
229 .into()),
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn required_permission_read_ops() {
239 assert_eq!(required_permission("/api/read"), Some("read"));
240 assert_eq!(required_permission("/api/nav"), Some("read"));
241 assert_eq!(required_permission("/api/defs"), Some("read"));
242 assert_eq!(required_permission("/api/libs"), Some("read"));
243 assert_eq!(required_permission("/api/hisRead"), Some("read"));
244 assert_eq!(required_permission("/api/watchSub"), Some("read"));
245 assert_eq!(required_permission("/api/watchPoll"), Some("read"));
246 assert_eq!(required_permission("/api/watchUnsub"), Some("read"));
247 assert_eq!(required_permission("/api/close"), Some("read"));
248 assert_eq!(required_permission("/api/about"), Some("read"));
249 assert_eq!(required_permission("/api/ops"), Some("read"));
250 assert_eq!(required_permission("/api/formats"), Some("read"));
251 }
252
253 #[test]
254 fn required_permission_write_ops() {
255 assert_eq!(required_permission("/api/pointWrite"), Some("write"));
256 assert_eq!(required_permission("/api/hisWrite"), Some("write"));
257 assert_eq!(required_permission("/api/invokeAction"), Some("write"));
258 assert_eq!(required_permission("/api/import"), Some("write"));
259 assert_eq!(required_permission("/api/federation/sync"), Some("write"));
260 }
261
262 #[test]
263 fn required_permission_admin_ops() {
264 assert_eq!(required_permission("/api/system/backup"), Some("admin"));
265 assert_eq!(required_permission("/api/system/restart"), Some("admin"));
266 assert_eq!(required_permission("/api/system/"), Some("admin"));
267 }
268
269 use crate::auth::users::hash_password;
272 use crate::ws::WatchManager;
273 use actix_web::dev::Service;
274 use actix_web::middleware::from_fn;
275 use actix_web::test as actix_test;
276 use actix_web::{App, HttpResponse};
277
278 fn test_state() -> web::Data<AppState> {
280 let hash = hash_password("s3cret");
281 let toml_str = format!(
282 r#"
283[users.admin]
284password_hash = "{hash}"
285permissions = ["read", "write", "admin"]
286
287[users.viewer]
288password_hash = "{hash}"
289permissions = ["read"]
290"#
291 );
292 let auth = AuthManager::from_toml_str(&toml_str).unwrap();
293 web::Data::new(AppState {
294 graph: haystack_core::graph::SharedGraph::new(haystack_core::graph::EntityGraph::new()),
295 namespace: parking_lot::RwLock::new(DefNamespace::new()),
296 auth,
297 watches: WatchManager::new(),
298 actions: ActionRegistry::new(),
299 his: HisStore::new(),
300 started_at: std::time::Instant::now(),
301 federation: Federation::new(),
302 })
303 }
304
305 fn insert_token(
307 state: &web::Data<AppState>,
308 username: &str,
309 permissions: Vec<String>,
310 ) -> String {
311 let token = uuid::Uuid::new_v4().to_string();
312 let user = crate::auth::AuthUser {
313 username: username.to_string(),
314 permissions,
315 };
316 state.auth.inject_token(token.clone(), user);
317 token
318 }
319
320 async fn dummy_handler() -> HttpResponse {
322 HttpResponse::Ok().body("ok")
323 }
324
325 fn test_app(
327 state: web::Data<AppState>,
328 ) -> App<
329 impl actix_web::dev::ServiceFactory<
330 ServiceRequest,
331 Config = (),
332 Response = ServiceResponse<impl MessageBody>,
333 Error = actix_web::Error,
334 InitError = (),
335 >,
336 > {
337 App::new()
338 .app_data(state)
339 .wrap(from_fn(auth_middleware))
340 .route("/api/about", web::get().to(dummy_handler))
341 .route("/api/ops", web::get().to(dummy_handler))
342 .route("/api/formats", web::get().to(dummy_handler))
343 .route("/api/read", web::post().to(dummy_handler))
344 .route("/api/hisRead", web::post().to(dummy_handler))
345 .route("/api/pointWrite", web::post().to(dummy_handler))
346 .route("/api/hisWrite", web::post().to(dummy_handler))
347 .route("/api/invokeAction", web::post().to(dummy_handler))
348 .route("/api/close", web::post().to(dummy_handler))
349 .route("/api/system/backup", web::post().to(dummy_handler))
350 }
351
352 async fn call_status(
355 app: &impl Service<
356 actix_http::Request,
357 Response = ServiceResponse<impl MessageBody>,
358 Error = actix_web::Error,
359 >,
360 req: actix_http::Request,
361 ) -> u16 {
362 match app.call(req).await {
363 Ok(resp) => resp.status().as_u16(),
364 Err(err) => err.as_response_error().status_code().as_u16(),
365 }
366 }
367
368 #[actix_rt::test]
369 async fn about_passes_through_without_auth() {
370 let state = test_state();
371 let app = actix_test::init_service(test_app(state)).await;
372
373 let req = actix_test::TestRequest::get()
374 .uri("/api/about")
375 .to_request();
376 assert_eq!(call_status(&app, req).await, 200);
377 }
378
379 #[actix_rt::test]
380 async fn ops_passes_through_without_auth() {
381 let state = test_state();
382 let app = actix_test::init_service(test_app(state)).await;
383
384 let req = actix_test::TestRequest::get().uri("/api/ops").to_request();
385 assert_eq!(call_status(&app, req).await, 200);
386 }
387
388 #[actix_rt::test]
389 async fn formats_passes_through_without_auth() {
390 let state = test_state();
391 let app = actix_test::init_service(test_app(state)).await;
392
393 let req = actix_test::TestRequest::get()
394 .uri("/api/formats")
395 .to_request();
396 assert_eq!(call_status(&app, req).await, 200);
397 }
398
399 #[actix_rt::test]
400 async fn protected_endpoint_without_token_returns_401() {
401 let state = test_state();
402 let app = actix_test::init_service(test_app(state)).await;
403
404 let req = actix_test::TestRequest::post()
405 .uri("/api/read")
406 .to_request();
407 assert_eq!(call_status(&app, req).await, 401);
408 }
409
410 #[actix_rt::test]
411 async fn viewer_can_read() {
412 let state = test_state();
413 let token = insert_token(&state, "viewer", vec!["read".to_string()]);
414 let app = actix_test::init_service(test_app(state)).await;
415
416 let req = actix_test::TestRequest::post()
417 .uri("/api/read")
418 .insert_header(("Authorization", format!("BEARER authToken={token}")))
419 .to_request();
420 assert_eq!(call_status(&app, req).await, 200);
421 }
422
423 #[actix_rt::test]
424 async fn viewer_cannot_write() {
425 let state = test_state();
426 let token = insert_token(&state, "viewer", vec!["read".to_string()]);
427 let app = actix_test::init_service(test_app(state)).await;
428
429 let req = actix_test::TestRequest::post()
430 .uri("/api/pointWrite")
431 .insert_header(("Authorization", format!("BEARER authToken={token}")))
432 .to_request();
433 assert_eq!(call_status(&app, req).await, 403);
434 }
435
436 #[actix_rt::test]
437 async fn viewer_cannot_invoke_action() {
438 let state = test_state();
439 let token = insert_token(&state, "viewer", vec!["read".to_string()]);
440 let app = actix_test::init_service(test_app(state)).await;
441
442 let req = actix_test::TestRequest::post()
443 .uri("/api/invokeAction")
444 .insert_header(("Authorization", format!("BEARER authToken={token}")))
445 .to_request();
446 assert_eq!(call_status(&app, req).await, 403);
447 }
448
449 #[actix_rt::test]
450 async fn viewer_cannot_his_write() {
451 let state = test_state();
452 let token = insert_token(&state, "viewer", vec!["read".to_string()]);
453 let app = actix_test::init_service(test_app(state)).await;
454
455 let req = actix_test::TestRequest::post()
456 .uri("/api/hisWrite")
457 .insert_header(("Authorization", format!("BEARER authToken={token}")))
458 .to_request();
459 assert_eq!(call_status(&app, req).await, 403);
460 }
461
462 #[actix_rt::test]
463 async fn writer_can_write() {
464 let state = test_state();
465 let token = insert_token(
466 &state,
467 "writer",
468 vec!["read".to_string(), "write".to_string()],
469 );
470 let app = actix_test::init_service(test_app(state)).await;
471
472 let req = actix_test::TestRequest::post()
473 .uri("/api/pointWrite")
474 .insert_header(("Authorization", format!("BEARER authToken={token}")))
475 .to_request();
476 assert_eq!(call_status(&app, req).await, 200);
477 }
478
479 #[actix_rt::test]
480 async fn admin_can_access_system() {
481 let state = test_state();
482 let token = insert_token(&state, "admin", vec!["admin".to_string()]);
483 let app = actix_test::init_service(test_app(state)).await;
484
485 let req = actix_test::TestRequest::post()
486 .uri("/api/system/backup")
487 .insert_header(("Authorization", format!("BEARER authToken={token}")))
488 .to_request();
489 assert_eq!(call_status(&app, req).await, 200);
490 }
491
492 #[actix_rt::test]
493 async fn viewer_cannot_access_system() {
494 let state = test_state();
495 let token = insert_token(&state, "viewer", vec!["read".to_string()]);
496 let app = actix_test::init_service(test_app(state)).await;
497
498 let req = actix_test::TestRequest::post()
499 .uri("/api/system/backup")
500 .insert_header(("Authorization", format!("BEARER authToken={token}")))
501 .to_request();
502 assert_eq!(call_status(&app, req).await, 403);
503 }
504
505 #[actix_rt::test]
506 async fn writer_cannot_access_system() {
507 let state = test_state();
508 let token = insert_token(
509 &state,
510 "writer",
511 vec!["read".to_string(), "write".to_string()],
512 );
513 let app = actix_test::init_service(test_app(state)).await;
514
515 let req = actix_test::TestRequest::post()
516 .uri("/api/system/backup")
517 .insert_header(("Authorization", format!("BEARER authToken={token}")))
518 .to_request();
519 assert_eq!(call_status(&app, req).await, 403);
520 }
521
522 #[actix_rt::test]
523 async fn viewer_can_close() {
524 let state = test_state();
525 let token = insert_token(&state, "viewer", vec!["read".to_string()]);
526 let app = actix_test::init_service(test_app(state)).await;
527
528 let req = actix_test::TestRequest::post()
529 .uri("/api/close")
530 .insert_header(("Authorization", format!("BEARER authToken={token}")))
531 .to_request();
532 assert_eq!(call_status(&app, req).await, 200);
533 }
534
535 #[actix_rt::test]
536 async fn viewer_can_his_read() {
537 let state = test_state();
538 let token = insert_token(&state, "viewer", vec!["read".to_string()]);
539 let app = actix_test::init_service(test_app(state)).await;
540
541 let req = actix_test::TestRequest::post()
542 .uri("/api/hisRead")
543 .insert_header(("Authorization", format!("BEARER authToken={token}")))
544 .to_request();
545 assert_eq!(call_status(&app, req).await, 200);
546 }
547
548 #[actix_rt::test]
549 async fn invalid_token_returns_401() {
550 let state = test_state();
551 let app = actix_test::init_service(test_app(state)).await;
552
553 let req = actix_test::TestRequest::post()
554 .uri("/api/read")
555 .insert_header(("Authorization", "BEARER authToken=bogus-token"))
556 .to_request();
557 assert_eq!(call_status(&app, req).await, 401);
558 }
559}