feat(server): add CredentialValidator trait for server-side auth#1172
feat(server): add CredentialValidator trait for server-side auth#1172Greg Lamberson (glamberson) wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a server-side hook to validate client-supplied credentials (from ClientInfoPdu) during connection setup, enabling external authentication backends (PAM/LDAP/DB) in ironrdp-server.
Changes:
- Introduces a new public
CredentialValidatortrait. - Adds an optional validator to
RdpServerwith a setter API. - Invokes credential validation in
client_acceptedbefore entering the session loop.
💡 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.
- CredentialValidator trait in ironrdp-server for TLS-mode credential validation against external systems (PAM, LDAP, gateway RADIUS) - CredentialProvider trait in ironrdp-acceptor for CredSSP/NLA runtime credential resolution (replaces static-only credentials) - Server wires both into RdpServer with set_credential_validator() and set_credential_provider() methods - Acceptor credential check now chains: provider -> static creds -> allow Based on upstream PR Devolutions#1172 (glamberson) and formalco fork design. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4535837 to
0baf0db
Compare
|
Rebased. Thanks. |
0baf0db to
1a38038
Compare
1a38038 to
3dfc431
Compare
|
Benoît Cortier (@CBenoit) Hi, I hope you can consider this with some priority as it's a feature I require in both lamco-rdp-server and lamco-qemu-rdp (Proxmox RDP Server component) and currently resides in a fork which I'd like to eliminate. Thanks! |
picky 7.0.0-rc.22 transitively required rand 0.10.0-rc.6 → rand_core pre-release → getrandom =0.4.0-rc.0, which conflicted with downstream crates depending on ashpd 0.13 (getrandom ^0.4 → 0.4.2 stable). Cargo's pre-release semver in resolver 3 refuses to coexist these. picky 7.0.0-rc.23 (published 2026-04-21) uses stable RustCrypto deps: rand ^0.10 (stable), rand_core ^0.10 (stable), pulling getrandom ^0.4 which resolves cleanly to the same 0.4.x ashpd uses. Local fork-only change to feat/lamco-combined; should be filed as a separate upstream PR once Devolutions#1172 (CredentialValidator) lands and we drop the fork.
…root The wildcard `pub use server::*` re-export in lib.rs was replaced with an explicit list upstream (Devolutions#1270 wildcard cleanup, merged 2026-05-13). The CredentialValidator commit predated that change, so the rebase onto current master left the trait defined but not exported. Adds CredentialValidator to the explicit re-export list so downstream consumers (e.g., lamco-rdp-server/src/security/auth.rs) can resolve it via `ironrdp_server::CredentialValidator`. Folds into PR Devolutions#1172 when the fork retires.
|
Benoît Cortier (@CBenoit) Hi, could you please review this and merge it ASAP? This one is a real bottleneck for me. Do you have any issues or concerns you'd like me to address or any issues with merging otherwise? Please let me know if so. Thanks. |
1300186 to
02672ca
Compare
picky 7.0.0-rc.22 transitively required rand 0.10.0-rc.6 → rand_core pre-release → getrandom =0.4.0-rc.0, which conflicted with downstream crates depending on ashpd 0.13 (getrandom ^0.4 → 0.4.2 stable). Cargo's pre-release semver in resolver 3 refuses to coexist these. picky 7.0.0-rc.23 (published 2026-04-21) uses stable RustCrypto deps: rand ^0.10 (stable), rand_core ^0.10 (stable), pulling getrandom ^0.4 which resolves cleanly to the same 0.4.x ashpd uses. Local fork-only change to feat/lamco-combined; should be filed as a separate upstream PR once Devolutions#1172 (CredentialValidator) lands and we drop the fork.
…root The wildcard `pub use server::*` re-export in lib.rs was replaced with an explicit list upstream (Devolutions#1270 wildcard cleanup, merged 2026-05-13). The CredentialValidator commit predated that change, so the rebase onto current master left the trait defined but not exported. Adds CredentialValidator to the explicit re-export list so downstream consumers (e.g., lamco-rdp-server/src/security/auth.rs) can resolve it via `ironrdp_server::CredentialValidator`. Folds into PR Devolutions#1172 when the fork retires.
Add a CredentialValidator trait that allows servers to validate client credentials during connection setup. Called after the acceptor phase extracts credentials from ClientInfoPdu. This enables PAM, LDAP, and other validate-on-receipt auth flows for TLS-mode connections. Not used for CredSSP/Hybrid connections which handle authentication through NTLM challenge-response. - Add CredentialValidator trait with validate() method - Add set_credential_validator() to RdpServer - Validate credentials in client_accepted() before session start
- 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
Devolutions#1270 (wildcard cleanup) replaced `pub use server::*` with an explicit re-export list. This PR predates that change, so without an update the trait would be defined in server.rs but unreachable from downstream consumers via `ironrdp_server::CredentialValidator`. Adds CredentialValidator to the explicit list in lib.rs.
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.
02672ca to
1f8d501
Compare
|
Benoît Cortier (@CBenoit) Today is 2026-05-25. PR #1172 has been open since 2026-03-17, which is 70 days. CI is green across all 20 checks on the current head ( I have pinged twice (2026-05-16, 2026-05-20). Both pings explicitly invited "what do you want me to change" feedback. Neither got a response. In the same five-day window since the 2026-05-20 ping, you have merged eight of my other PRs, including a Saturday-filed one (#1303) that landed Monday morning. The signal is that this specific PR is being deferred, not lost in queue. I respect that Devolutions has priorities I do not see and competing directions I am not party to. Those are legitimate. I have tried to bend over backwards to align my work with what I understand about your priorities: the precheck-driven amend cadence, the testsuite-core test-placement convention, the typed-error-no-anyhow direction, the conventional-commit + What I want is engagement, not approval. The options I see, from my side:
Competing priorities, conflicting thoughts, structural concerns, scope disagreements: all of these are fine and I welcome the discussion. Silence is what is not working here. What is the path forward? |
Closes #1154
Summary
Add a
CredentialValidatortrait that lets servers validate clientcredentials during connection setup, after the acceptor extracts them
from
ClientInfoPdu(enabled by #1155).This follows up on the API discussion in #1150:
Option<Credentials>is the wrong type to carry security policy. The trait makes auth an
explicit, application-supplied component rather than an implicit
builder default.
What it does
CredentialValidatortrait withvalidate(&self, &Credentials) -> Result<CredentialDecision, CredentialValidationError>.CredentialDecision::{Accept, Reject}enum for the binary outcome (no stringly typed bool).CredentialValidationErrorwrapping anycore::error::Error + Send + Syncfor backend-failure cases (LDAP/PAM/DB unreachable). ManualDisplay+core::error::Errorimpls; the source chain is preserved through.source().RdpServerBuilder::with_credential_validator(Option<Arc<dyn CredentialValidator>>)to wire it in at construction time, matching thewith_*pattern of every other configurable onBuilderDone.RdpServer::set_credential_validator(Option<Arc<dyn CredentialValidator>>)for post-construction reconfiguration.client_accepted()before session start.Decision::Rejector backendErr(_).AcceptorResult.credentialsisNoneper fix(acceptor): skip credential check when server credentials are None #1150/feat(acceptor): expose received client credentials in AcceptorResult #1155).Design
For TLS-only connections, Client Info credentials are the only
credentials the application layer receives (no CredSSP). This trait
gives server implementations a clean hook to validate them against
PAM, LDAP, databases, or any external system.
Not used for CredSSP/Hybrid connections, where authentication happens
during the NTLM challenge-response exchange before
ClientInfoPdu.Rejection is modelled as
Ok(CredentialDecision::Reject), not as anErr. A rejection from a working validator is the validator doingits job, not a failure of the validator itself. Only true backend
infrastructure failures (the LDAP server is down, the PAM transport
broke) return
Err(CredentialValidationError::new(_)). This keepsthe three outcomes (accept / reject / infrastructure-error) distinct
at every call site.
Hand-rolled
Display+core::error::Errorimpls follow thepost-#1264 workspace pattern; no
thiserror, noanyhowleakageacross the public trait boundary.
Example
Tests
Public-API tests for the trait,
CredentialDecision, and the errorwrapper's source-chain propagation 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)]for crate-internalbehavior, per the existing
autodetect.rsprecedent).Test plan
cargo xtask check fmt/lints/tests/typos/locksall pass.