feat(acceptor): expose received client credentials in AcceptorResult#1155
Conversation
|
Thanks!
This sounds like an API breakage because |
There was a problem hiding this comment.
Pull request overview
This PR adds an Option<Credentials> field to AcceptorResult in the ironrdp-acceptor crate, allowing server implementations to access client-supplied credentials after the RDP handshake completes. This is an interim solution enabling post-handshake credential validation (e.g., PAM, LDAP) for TLS-mode connections, as described in issue #1154.
Changes:
- Stores received client credentials during
SecureSettingsExchange(TLS-mode only) in a new privatereceived_credentialsfield onAcceptor, and exposes them via a new publiccredentials: Option<Credentials>field onAcceptorResult. - Propagates stored credentials through
new_deactivation_reactivationfor server reactivation flows.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
|
I’m not against the change, but the commit and PR should be updated to reflect that the change is breaking. Also you can fix the place spotted by Copilot |
|
Also note that we already merged work to address #1154 in the proper way in sspi-rs, and we could expect a proper solution in the near future as Guillaume Gelin (@ramnes) is already working on it. Do you still want to merge the stopgap? Might be unnecessary as I would wait for the Guillaume Gelin (@ramnes) changes before releasing the new IronRDP crates. |
Add an optional `credentials` field to `AcceptorResult` that contains the client credentials received during SecureSettingsExchange. For TLS-mode connections, the client sends credentials in the ClientInfoPdu. This field exposes them so servers can validate against external systems (PAM, LDAP, etc.) after the handshake. None for CredSSP/Hybrid connections where authentication happens during the CredSSP exchange instead. BREAKING CHANGE: AcceptorResult has a new public field. Code that destructures AcceptorResult directly will need to be updated.
854e6a7 to
d940b5b
Compare
|
Yeah, still need this. Right now the acceptor extracts credentials from ClientInfoPdu but doesn't expose them. That blocks my PAM auth flow entirely. I've been doing exhaustive testing across 11 Linux VMs (Fedora, Ubuntu, Arch, RHEL, Tumbleweed, Debian, EndeavourOS, etc.) and this is a P1 blocker for all of them (lamco-admin/lamco-rdp-server#35). i'm happy to migrate to Guillaume Gelin (@ramnes)'s proper solution when it lands, but this is a one field addition that unblocks me today. The field would still be useful alongside a validator trait anyway since i'd want to inspect credentials for audit logging regardless. Thanks for the quick review btw, really appreciate it. Updated the commit to |
- Skip validation when credentials are None (reactivation, CredSSP) instead of bailing, consistent with Devolutions#1150/Devolutions#1155 design - Remove username from log messages to avoid leaking sensitive data - Keep validator error details in structured tracing field only - Add spawn_blocking guidance to trait doc for blocking backends
- Skip validation when credentials are None (reactivation, CredSSP) instead of bailing, consistent with Devolutions#1150/Devolutions#1155 design - Remove username from log messages to avoid leaking sensitive data - Keep validator error details in structured tracing field only - Add spawn_blocking guidance to trait doc for blocking backends
- Skip validation when credentials are None (reactivation, CredSSP) instead of bailing, consistent with Devolutions#1150/Devolutions#1155 design - Remove username from log messages to avoid leaking sensitive data - Keep validator error details in structured tracing field only - Add spawn_blocking guidance to trait doc for blocking backends
- Skip validation when credentials are None (reactivation, CredSSP) instead of bailing, consistent with Devolutions#1150/Devolutions#1155 design - Remove username from log messages to avoid leaking sensitive data - Keep validator error details in structured tracing field only - Add spawn_blocking guidance to trait doc for blocking backends
- Skip validation when credentials are None (reactivation, CredSSP) instead of bailing, consistent with Devolutions#1150/Devolutions#1155 design - Remove username from log messages to avoid leaking sensitive data - Keep validator error details in structured tracing field only - Add spawn_blocking guidance to trait doc for blocking backends
- Skip validation when credentials are None (reactivation, CredSSP) instead of bailing, consistent with Devolutions#1150/Devolutions#1155 design - Remove username from log messages to avoid leaking sensitive data - Keep validator error details in structured tracing field only - Add spawn_blocking guidance to trait doc for blocking backends
Reshape the public surface introduced earlier in this PR before it lands, to avoid a future breaking touch when ironrdp-server's anyhow-removal pass arrives. Mirrors the post-Devolutions#1264 hand-rolled error pattern (Display + core::error::Error impls, no thiserror). Trait signature was `Result<bool>` (i.e. anyhow::Result through the file-level `use anyhow::Result`). Two issues: - New public surface depending on anyhow at a moment when the workspace is moving the other way (Devolutions#1277 just removed anyhow from rdpsnd-native). - `Ok(true)` vs `Ok(false)` is stringly typed at the call site: a bare bool with no semantic clue. New shape: - `CredentialDecision::{Accept, Reject}` for the binary outcome. - `CredentialValidationError` wrapping any `core::error::Error + Send + Sync` for the case where the validator backend itself failed (LDAP unreachable, PAM transport error, etc.). Manual Display + Error impls; source chains through. - `validate(&self, &Credentials) -> Result<CredentialDecision, CredentialValidationError>`. The accept/reject/backend-error trichotomy is now explicit at every call site. Rejection is no longer an error; backend failure is. Log + bail strings updated to match the trichotomy. Also addresses the API consistency note: every other configurable on `RdpServerBuilder<BuilderDone>` goes through `with_*` on the builder. Added `with_credential_validator(Option<Arc<dyn ...>>)` following the same shape as `with_connection_handler`. The post-construction `set_credential_validator` setter is kept (now takes `Option` to match the field and the builder's setter shape) for dynamic reconfiguration after `build()`. Tests added in `server.rs` cover Accept / Reject / backend-error propagation and `Arc<dyn CredentialValidator>` Send + Sync + 'static bounds. Integration with `client_accepted` is exercised through the existing acceptor-side tests once the validator is wired via the builder; no new integration test in this commit (server-side precedent per Devolutions#1181 / Devolutions#1187 / Devolutions#1281). Verification: `cargo test -p ironrdp-server --lib` (14 passed), `cargo clippy -p ironrdp-server --all-targets -- -D warnings` clean, `cargo check -p ironrdp-server` clean. Refs Devolutions#1154, Devolutions#1150, Devolutions#1155.
Reshape the public surface introduced earlier in this PR before it lands, to avoid a future breaking touch when ironrdp-server's anyhow-removal pass arrives. Mirrors the post-Devolutions#1264 hand-rolled error pattern (Display + core::error::Error impls, no thiserror). Trait signature was `Result<bool>` (i.e. anyhow::Result through the file-level `use anyhow::Result`). Two issues: - New public surface depending on anyhow at a moment when the workspace is moving the other way (Devolutions#1277 just removed anyhow from rdpsnd-native). - `Ok(true)` vs `Ok(false)` is stringly typed at the call site: a bare bool with no semantic clue. New shape: - `CredentialDecision::{Accept, Reject}` for the binary outcome. - `CredentialValidationError` wrapping any `core::error::Error + Send + Sync` for the case where the validator backend itself failed (LDAP unreachable, PAM transport error, etc.). Manual Display + Error impls; source chains through. - `validate(&self, &Credentials) -> Result<CredentialDecision, CredentialValidationError>`. The accept/reject/backend-error trichotomy is now explicit at every call site. Rejection is no longer an error; backend failure is. Log + bail strings updated to match the trichotomy. Also addresses the API consistency note: every other configurable on `RdpServerBuilder<BuilderDone>` goes through `with_*` on the builder. Added `with_credential_validator(Option<Arc<dyn ...>>)` following the same shape as `with_connection_handler`. The post-construction `set_credential_validator` setter is kept (now takes `Option` to match the field and the builder's setter shape) for dynamic reconfiguration after `build()`. Tests for the trait, the `CredentialDecision` enum, the error wrapper's source chaining, and the `Send + Sync + 'static` bounds through `Arc<dyn _>` live in `crates/ironrdp-testsuite-core/tests/server/credential_validator.rs`, matching the canonical IronRDP split: public-API tests in `ironrdp-testsuite-core`, inline `#[cfg(test)]` only for crate-internal behavior (per the `autodetect.rs` precedent which has both: inline tests on internal state machine, public-API tests in `testsuite-core/tests/server/autodetect.rs`). Verification: `cargo xtask check fmt/lints/tests/typos/locks` all pass; new tests pass in the testsuite-core harness (4 added). Refs Devolutions#1154, Devolutions#1150, Devolutions#1155.
Reshape the public surface introduced earlier in this PR before it lands, to avoid a future breaking touch when ironrdp-server's anyhow-removal pass arrives. Mirrors the post-Devolutions#1264 hand-rolled error pattern (Display + core::error::Error impls, no thiserror). Trait signature was `Result<bool>` (i.e. anyhow::Result through the file-level `use anyhow::Result`). Two issues: - New public surface depending on anyhow at a moment when the workspace is moving the other way (Devolutions#1277 just removed anyhow from rdpsnd-native). - `Ok(true)` vs `Ok(false)` is stringly typed at the call site: a bare bool with no semantic clue. New shape: - `CredentialDecision::{Accept, Reject}` for the binary outcome. - `CredentialValidationError` wrapping any `core::error::Error + Send + Sync` for the case where the validator backend itself failed (LDAP unreachable, PAM transport error, etc.). Manual Display + Error impls; source chains through. - `validate(&self, &Credentials) -> Result<CredentialDecision, CredentialValidationError>`. The accept/reject/backend-error trichotomy is now explicit at every call site. Rejection is no longer an error; backend failure is. Log + bail strings updated to match the trichotomy. Also addresses the API consistency note: every other configurable on `RdpServerBuilder<BuilderDone>` goes through `with_*` on the builder. Added `with_credential_validator(Option<Arc<dyn ...>>)` following the same shape as `with_connection_handler`. The post-construction `set_credential_validator` setter is kept (now takes `Option` to match the field and the builder's setter shape) for dynamic reconfiguration after `build()`. Tests for the trait, the `CredentialDecision` enum, the error wrapper's source chaining, and the `Send + Sync + 'static` bounds through `Arc<dyn _>` live in `crates/ironrdp-testsuite-core/tests/server/credential_validator.rs`, matching the canonical IronRDP split: public-API tests in `ironrdp-testsuite-core`, inline `#[cfg(test)]` only for crate-internal behavior (per the `autodetect.rs` precedent which has both: inline tests on internal state machine, public-API tests in `testsuite-core/tests/server/autodetect.rs`). Verification: `cargo xtask check fmt/lints/tests/typos/locks` all pass; new tests pass in the testsuite-core harness (4 added). Refs Devolutions#1154, Devolutions#1150, Devolutions#1155.
- Skip validation when credentials are None (reactivation, CredSSP) instead of bailing, consistent with Devolutions#1150/Devolutions#1155 design - Remove username from log messages to avoid leaking sensitive data - Keep validator error details in structured tracing field only - Add spawn_blocking guidance to trait doc for blocking backends
- Skip validation when credentials are None (reactivation, CredSSP) instead of bailing, consistent with Devolutions#1150/Devolutions#1155 design - Remove username from log messages to avoid leaking sensitive data - Keep validator error details in structured tracing field only - Add spawn_blocking guidance to trait doc for blocking backends
Reshape the public surface introduced earlier in this PR before it lands, to avoid a future breaking touch when ironrdp-server's anyhow-removal pass arrives. Mirrors the post-Devolutions#1264 hand-rolled error pattern (Display + core::error::Error impls, no thiserror). Trait signature was `Result<bool>` (i.e. anyhow::Result through the file-level `use anyhow::Result`). Two issues: - New public surface depending on anyhow at a moment when the workspace is moving the other way (Devolutions#1277 just removed anyhow from rdpsnd-native). - `Ok(true)` vs `Ok(false)` is stringly typed at the call site: a bare bool with no semantic clue. New shape: - `CredentialDecision::{Accept, Reject}` for the binary outcome. - `CredentialValidationError` wrapping any `core::error::Error + Send + Sync` for the case where the validator backend itself failed (LDAP unreachable, PAM transport error, etc.). Manual Display + Error impls; source chains through. - `validate(&self, &Credentials) -> Result<CredentialDecision, CredentialValidationError>`. The accept/reject/backend-error trichotomy is now explicit at every call site. Rejection is no longer an error; backend failure is. Log + bail strings updated to match the trichotomy. Also addresses the API consistency note: every other configurable on `RdpServerBuilder<BuilderDone>` goes through `with_*` on the builder. Added `with_credential_validator(Option<Arc<dyn ...>>)` following the same shape as `with_connection_handler`. The post-construction `set_credential_validator` setter is kept (now takes `Option` to match the field and the builder's setter shape) for dynamic reconfiguration after `build()`. Tests for the trait, the `CredentialDecision` enum, the error wrapper's source chaining, and the `Send + Sync + 'static` bounds through `Arc<dyn _>` live in `crates/ironrdp-testsuite-core/tests/server/credential_validator.rs`, matching the canonical IronRDP split: public-API tests in `ironrdp-testsuite-core`, inline `#[cfg(test)]` only for crate-internal behavior (per the `autodetect.rs` precedent which has both: inline tests on internal state machine, public-API tests in `testsuite-core/tests/server/autodetect.rs`). Verification: `cargo xtask check fmt/lints/tests/typos/locks` all pass; new tests pass in the testsuite-core harness (4 added). Refs Devolutions#1154, Devolutions#1150, Devolutions#1155.
Summary
Adds
credentials: Option<Credentials>toAcceptorResultso servers can access the credentials received from the client duringSecureSettingsExchange.This is a companion to #1150 and an interim solution for #1154. Together, they enable servers to implement post-handshake credential validation (PAM, LDAP, etc.) for TLS-mode connections.
Changes
received_credentials: Option<Credentials>field toAcceptor(private)SecureSettingsExchange(connection.rs, TLS-mode only)credentials: Option<Credentials>field toAcceptorResult(public)get_result()intoAcceptorResultnew_deactivation_reactivationBehavior
credentialsisSome(...)containing the credentials fromClientInfoPducredentialsisNone(authentication happens via CredSSP, notClientInfoPdu)AcceptorResultis not#[non_exhaustive], so adding a new public field is semver-breaking. Existing code that destructuresAcceptorResultwill need to add the field.Context
This is explicitly a stopgap for #1154, which proposes a
CredentialValidatortrait as the proper long-term solution. This PR enables the use case immediately with minimal changes while the trait design is discussed.Use case
I'm building a standalone Linux RDP server using IronRDP. For Linux authentication, I need to validate client-supplied credentials against PAM (
pam_authenticate). The current acceptor compares credentials and drops them; after the handshake there's no way to retrieve what the client sent. This PR retains them inAcceptorResultso the server can extract and validate them post-handshake.Checks
cargo xtask check fmt -v: passcargo xtask check lints -v: passcargo xtask check tests -v: passRef: #1154, #1149, #1150