Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/ironposh-client-core/src/connector/auth_sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ impl SspiAuthSequence {
match res {
super::authenticator::ActionReqired::TryInitSecContextAgain { token } => {
self.http_builder.with_auth_header(token.0);
let body =
operation_body.map_or_else(HttpBody::empty, |xml| HttpBody::Xml(xml.to_owned()));
let body = operation_body
.map_or_else(HttpBody::empty, |xml| HttpBody::Xml(xml.to_owned()));
Ok(SecCtxInited::Continue(self.http_builder.post(body)))
}
super::authenticator::ActionReqired::Done { token } => Ok(SecCtxInited::Done(token)),
Expand Down
8 changes: 5 additions & 3 deletions crates/ironposh-client-core/src/connector/connection_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ impl ConnectionId {
pub enum ConnectionState {
/// SSPI only. Retains the queued SOAP so a TLS channel-binding challenge can
/// restart auth on a fresh connection with the binding applied.
PreAuth { queued_xml: String },
PreAuth {
queued_xml: String,
},
Idle {
enc: EncryptionOptions,
},
Expand Down Expand Up @@ -688,8 +690,8 @@ impl PostConAuthSequence {
// When sealing is off (HTTPS), the operation SOAP must ride the auth
// challenge legs (the server rejects a token-less operation request).
// Clone it so we can hand a copy to each leg without borrow conflicts.
let operation_body = (!self.auth_sequence.require_encryption())
.then(|| self.queued_xml.clone());
let operation_body =
(!self.auth_sequence.require_encryption()).then(|| self.queued_xml.clone());

match self
.auth_sequence
Expand Down
5 changes: 4 additions & 1 deletion crates/ironposh-client-core/src/host/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ pub fn test_write_progress_tolerates_nil_fields() {
let record = ComplexObject::standard()
.extended("Activity", "Preparing modules for first use.")
.extended("ActivityId", 0i32)
.extended("CurrentOperation", PsValue::Primitive(PsPrimitiveValue::Nil))
.extended(
"CurrentOperation",
PsValue::Primitive(PsPrimitiveValue::Nil),
)
.extended("ParentActivityId", -1i32)
.extended("PercentComplete", -1i32)
.extended("SecondsRemaining", -1i32)
Expand Down
10 changes: 4 additions & 6 deletions crates/ironposh-client-tokio/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,14 +307,12 @@ async fn main() -> anyhow::Result<()> {
// multiplexer erroring) and avoids racing the JoinHandle. Bounded so a
// pathological non-terminating task cannot hang process exit.
if connection_error.is_none() && !command_completed {
match tokio::time::timeout(
std::time::Duration::from_secs(5),
&mut connection_handle,
)
.await
match tokio::time::timeout(std::time::Duration::from_secs(5), &mut connection_handle)
.await
{
Ok(Ok(Ok(()))) => {
connection_error = Some("connection closed before the command completed".into());
connection_error =
Some("connection closed before the command completed".into());
}
Ok(Ok(Err(e))) => connection_error = Some(e.to_string()),
// The handle is awaited before any abort(), so a JoinError here is a
Expand Down
81 changes: 63 additions & 18 deletions crates/ironposh-client-tokio/tests/e2e/transport_auth_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ struct ClientRun {
fn run_cell(auth: &str, transport: &[&str], tag: &str) -> ClientRun {
let bin = env!("CARGO_BIN_EXE_ironposh-client-tokio");
let cfg = e2e_pwsh_config::load_from_env_or_default();
let log_path = std::env::temp_dir().join(format!(
"ironposh-matrix.{tag}.{}.log",
std::process::id()
));
let log_path =
std::env::temp_dir().join(format!("ironposh-matrix.{tag}.{}.log", std::process::id()));
let _ = std::fs::remove_file(&log_path);

let mut cmd = Command::new(bin);
Expand Down Expand Up @@ -93,24 +91,48 @@ fn tail(r: &ClientRun) -> String {
#[ignore = "e2e matrix test: requires reachable WinRM server (HTTP 5985 + HTTPS 5986)"]
fn negotiate_over_http_seals_messages() {
let r = run_cell("negotiate", HTTP, "neg-http");
assert!(connected(&r), "Negotiate/HTTP should authenticate and run\n{}", tail(&r));
assert!(sealed(&r), "Negotiate/HTTP MUST SSPI-seal the payload\n{}", tail(&r));
assert!(
connected(&r),
"Negotiate/HTTP should authenticate and run\n{}",
tail(&r)
);
assert!(
sealed(&r),
"Negotiate/HTTP MUST SSPI-seal the payload\n{}",
tail(&r)
);
}

#[test]
#[ignore = "e2e matrix test: requires reachable WinRM server (HTTP 5985 + HTTPS 5986)"]
fn kerberos_over_http_seals_messages() {
let r = run_cell("kerberos", HTTP, "krb-http");
assert!(connected(&r), "Kerberos/HTTP should authenticate and run\n{}", tail(&r));
assert!(sealed(&r), "Kerberos/HTTP MUST SSPI-seal the payload\n{}", tail(&r));
assert!(
connected(&r),
"Kerberos/HTTP should authenticate and run\n{}",
tail(&r)
);
assert!(
sealed(&r),
"Kerberos/HTTP MUST SSPI-seal the payload\n{}",
tail(&r)
);
}

#[test]
#[ignore = "e2e matrix test: requires reachable WinRM server (HTTP 5985 + HTTPS 5986)"]
fn ntlm_over_http_seals_messages() {
let r = run_cell("ntlm", HTTP, "ntlm-http");
assert!(connected(&r), "NTLM/HTTP should authenticate and run\n{}", tail(&r));
assert!(sealed(&r), "NTLM/HTTP MUST SSPI-seal the payload\n{}", tail(&r));
assert!(
connected(&r),
"NTLM/HTTP should authenticate and run\n{}",
tail(&r)
);
assert!(
sealed(&r),
"NTLM/HTTP MUST SSPI-seal the payload\n{}",
tail(&r)
);
}

// ──────────────────── HTTPS + SSPI → NOT sealed (TLS) ────────────────────
Expand All @@ -119,7 +141,11 @@ fn ntlm_over_http_seals_messages() {
#[ignore = "e2e matrix test: requires reachable WinRM server (HTTP 5985 + HTTPS 5986)"]
fn negotiate_over_https_does_not_seal() {
let r = run_cell("negotiate", HTTPS, "neg-https");
assert!(connected(&r), "Negotiate/HTTPS should authenticate and run\n{}", tail(&r));
assert!(
connected(&r),
"Negotiate/HTTPS should authenticate and run\n{}",
tail(&r)
);
assert!(
!sealed(&r),
"Negotiate/HTTPS MUST NOT SSPI-seal — TLS provides confidentiality (seal ⟂ TLS)\n{}",
Expand All @@ -131,7 +157,11 @@ fn negotiate_over_https_does_not_seal() {
#[ignore = "e2e matrix test: requires reachable WinRM server (HTTP 5985 + HTTPS 5986)"]
fn kerberos_over_https_does_not_seal() {
let r = run_cell("kerberos", HTTPS, "krb-https");
assert!(connected(&r), "Kerberos/HTTPS should authenticate and run\n{}", tail(&r));
assert!(
connected(&r),
"Kerberos/HTTPS should authenticate and run\n{}",
tail(&r)
);
assert!(
!sealed(&r),
"Kerberos/HTTPS MUST NOT SSPI-seal — TLS provides confidentiality (seal ⟂ TLS)\n{}",
Expand All @@ -143,7 +173,11 @@ fn kerberos_over_https_does_not_seal() {
#[ignore = "e2e matrix test: requires reachable WinRM server (HTTP 5985 + HTTPS 5986)"]
fn ntlm_over_https_does_not_seal() {
let r = run_cell("ntlm", HTTPS, "ntlm-https");
assert!(connected(&r), "NTLM/HTTPS should authenticate and run\n{}", tail(&r));
assert!(
connected(&r),
"NTLM/HTTPS should authenticate and run\n{}",
tail(&r)
);
assert!(
!sealed(&r),
"NTLM/HTTPS MUST NOT SSPI-seal — TLS provides confidentiality (seal ⟂ TLS)\n{}",
Expand All @@ -157,9 +191,14 @@ fn ntlm_over_https_does_not_seal() {
#[ignore = "e2e matrix test: requires reachable WinRM server (HTTP 5985 + HTTPS 5986)"]
fn basic_over_http_is_refused_without_force() {
let r = run_cell("basic", HTTP, "basic-http");
assert!(!r.success, "Basic over plain HTTP must be refused\n{}", tail(&r));
assert!(
r.output.contains("Basic authentication over plain HTTP is refused"),
!r.success,
"Basic over plain HTTP must be refused\n{}",
tail(&r)
);
assert!(
r.output
.contains("Basic authentication over plain HTTP is refused"),
"refusal must be explained to the user\n{}",
tail(&r)
);
Expand All @@ -170,7 +209,8 @@ fn basic_over_http_is_refused_without_force() {
fn basic_over_http_is_allowed_with_force_flag() {
let r = run_cell("basic", HTTP_FORCED, "basic-http-forced");
assert!(
!r.output.contains("Basic authentication over plain HTTP is refused"),
!r.output
.contains("Basic authentication over plain HTTP is refused"),
"--http-insecure must bypass the Basic-over-HTTP guard\n{}",
tail(&r)
);
Expand All @@ -188,7 +228,8 @@ fn basic_over_http_is_allowed_with_force_flag() {
fn basic_over_https_is_allowed_and_unsealed() {
let r = run_cell("basic", HTTPS, "basic-https");
assert!(
!r.output.contains("Basic authentication over plain HTTP is refused"),
!r.output
.contains("Basic authentication over plain HTTP is refused"),
"Basic over HTTPS must be permitted (TLS encrypts the credentials)\n{}",
tail(&r)
);
Expand All @@ -197,7 +238,11 @@ fn basic_over_https_is_allowed_and_unsealed() {
"Basic is never SSPI-sealed; over HTTPS confidentiality comes from TLS\n{}",
tail(&r)
);
assert!(reached_server(&r), "Basic/HTTPS should reach the server\n{}", tail(&r));
assert!(
reached_server(&r),
"Basic/HTTPS should reach the server\n{}",
tail(&r)
);
}

// ─────────────── Forced unencrypted SSPI over HTTP → not sealed ───────────────
Expand Down