use std::env; use latchd_auth::AuthService; use latchd_core::{ accounts::Identity, config::Config, error::Error, session::{self, SessionDescriptor, SessionKind}, }; fn fixture_service() -> String { env::var("LATCHD_PAM_FIXTURE_SERVICE") .expect("LATCHD_PAM_FIXTURE_SERVICE must point at the disposable PAM fixture") } fn session_fixture_service() -> String { env::var("LATCHD_PAM_SESSION_FIXTURE_SERVICE") .expect("LATCHD_PAM_SESSION_FIXTURE_SERVICE must point at the disposable PAM fixture") } fn assert_sanitized_pam_failure(result: Result<(), Error>, secret: &str) { let error = result.expect_err("the fault-injection PAM fixture must reject the operation"); match error { Error::Pam { status, message, .. } => { assert_ne!(status, 0); assert!(!message.contains(secret)); } _ => panic!("fault-injection fixture returned a non-PAM error"), } } #[test] #[ignore = "requires the disposable PAM fixture installed by the Docker/VM harness"] fn authentication_fault_is_rejected_without_secret_leakage() { let service = AuthService::new().expect("PAM worker should start"); let secret = "fixture-auth-secret"; let result = service.authenticate("root".into(), secret.as_bytes().to_vec(), fixture_service()); let error = match result { Ok(_) => panic!("pam_deny fixture must reject authentication"), Err(error) => error, }; match error { Error::Pam { status, message, .. } => { assert_ne!(status, 0); assert!(!message.contains(secret)); } _ => panic!("fault-injection fixture returned a non-PAM error"), } } #[test] #[ignore = "requires the disposable PAM fixture installed by the Docker/VM harness"] fn password_change_fault_is_rejected_without_secret_leakage() { let service = AuthService::new().expect("PAM worker should start"); assert_sanitized_pam_failure( service.change_password( "root".into(), b"fixture-old-secret".to_vec(), b"fixture-new-secret".to_vec(), fixture_service(), ), "fixture-new-secret", ); } #[test] #[ignore = "requires the disposable PAM fixture installed by the Docker/VM harness"] fn selected_session_metadata_reaches_pam_before_open() { let service = AuthService::new().expect("PAM worker should start"); let ticket = service .authenticate("root".into(), Vec::new(), session_fixture_service()) .expect("permit fixture should authenticate root"); let identity = Identity { username: "root".into(), uid: 0, gid: 0, home: "/root".into(), shell: "/bin/sh".into(), }; let descriptor = SessionDescriptor { kind: SessionKind::Wayland, title: "Niri fixture".into(), argv: vec!["/bin/true".into()], current_desktop: Some("niri".into()), session_desktop: Some("niri".into()), }; let spec = session::build_spec(&identity, &Config::default(), &descriptor) .expect("fixture session should build"); service .run_session(ticket, spec) .expect("PAM must receive the selected graphical session metadata"); } #[test] #[ignore = "requires the disposable PAM fixture installed by the Docker/VM harness"] fn pam_runtime_environment_reaches_the_session_child() { let service = AuthService::new().expect("PAM worker should start"); let ticket = service .authenticate("root".into(), Vec::new(), session_fixture_service()) .expect("permit fixture should authenticate root"); let identity = Identity { username: "root".into(), uid: 0, gid: 0, home: "/root".into(), shell: "/bin/sh".into(), }; let descriptor = SessionDescriptor { kind: SessionKind::Wayland, title: "PAM environment fixture".into(), argv: vec![ "/bin/sh".into(), "-c".into(), "test \"$XDG_SESSION_ID\" = fixture-session && \ test \"$XDG_RUNTIME_DIR\" = /tmp/latchd-runtime-fixture && \ test \"$DBUS_SESSION_BUS_ADDRESS\" = unix:path=/tmp/latchd-runtime-fixture/bus" .into(), ], current_desktop: Some("niri".into()), session_desktop: Some("niri".into()), }; let spec = session::build_spec(&identity, &Config::default(), &descriptor) .expect("fixture session should build"); service .run_session(ticket, spec) .expect("the child must receive runtime values produced by PAM"); }