feat(agent): support PSU gRPC app token secrets#1848
Conversation
Resolve configured PSU gRPC AppToken values before opening the agent stream so literal tokens are passed through and secret references reuse the existing PowerShell secret resolver. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Let maintainers know that an action is required on their side
|
There was a problem hiding this comment.
Pull request overview
This PR enables the PSU gRPC agent to authenticate with secret-backed application tokens. Previously, the gRPC agent forwarded the configured AppToken verbatim as bearer metadata, so a $secret:<name> reference would be sent literally instead of the resolved secret. The change resolves the token before opening the agent stream, reusing the existing PSU PowerShell secret resolver (already used by the PSU Event Hub compatibility feature) so literal tokens pass through unchanged and $secret:<name> references are resolved on demand. This fits into the devolutions-agent PSU integration, keeping token handling in the agent transport layer.
Changes:
- Add
PsuGrpcAgent::resolve_app_tokenand call it inrun_single_connectionso the resolved token (not the raw config value) is carried inauthorizationmetadata. - Widen visibility of
PowerShellWorker, its constructor/resolve_app_token, and the (renamed)app_token_secret_reference_namehelper topub(crate)so the gRPC agent module can reuse them. - Add unit tests covering literal-token pass-through and empty-token filtering.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| devolutions-agent/src/psu_grpc_agent/mod.rs | Adds resolve_app_token (literal vs $secret: handling), resolves the token before connecting, and adds two unit tests. |
| devolutions-agent/src/psu_event_hub/powershell_worker.rs | Widens PowerShellWorker/new/resolve_app_token to pub(crate) and renames secret_reference_name → app_token_secret_reference_name (crate-public); updates tests. |
| devolutions-agent/src/psu_event_hub/mod.rs | Exposes the powershell_worker module as pub(crate) so the sibling gRPC agent module can reference it. |
I verified all references to the renamed helper were updated, that widening visibility does not break existing consumers, that the new error messages follow the repo's lowercase/no-punctuation convention, and that the test struct literals match the dto definitions. No objective issues found.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Thank you Adam!
|
|
||
| async fn run_single_connection(&self, shutdown_signal: &mut ShutdownSignal) -> anyhow::Result<()> { | ||
| let app_token = self.resolve_app_token().await?; | ||
| let endpoint = Endpoint::from_shared(self.server_url.clone())?; |
There was a problem hiding this comment.
issue: I think app-token resolution runs on every reconnect, not once.
run_single_connection is called inside the retry loop in run() (in mod.rs).
For a $secret: token that means a fresh PowerShellWorker::new (temp .ps1 write) plus a pwsh subprocess spawn each cycle. "resolve ... before opening the agent stream" reads as a single pre-connect resolution, so this per-attempt behavior looks accidental. Contrast the event-hub path, which builds one shared worker and resolves once.
Probably another issue with this: because resolution sits inside the retried run_single_connection, a permanently misconfigured secret (missing vault, typo'd name…) is indistinguishable from a network failure: warn, back off, retry forever, spawning pwsh each cycle. The event-hub path instead logs and skips
the connection on resolution failure. For a single-connection agent "skip" would mean exiting the task, so retry may be the right call if the vault could come online later, but this deserves an explicit decision.
suggestion: Fix is small: resolve once in run() before the loop and pass the resolved token into
run_single_connection. Moving resolution out of the loop also lets a hard config error fail loudly once rather than looping quietly. If per-reconnect re-resolution is instead a deliberate secret-rotation feature, please signal the intent with inline comments.
thought: Also, if we want to distinguish which error are transients and which are fatal, we may consider returning something else than anyhow::Result. It’s generally a good idea when dealing with retry loops.
| mod executor; | ||
| mod models; | ||
| mod powershell_worker; | ||
| pub(crate) mod powershell_worker; |
There was a problem hiding this comment.
thought: PowerShellWorker now has two consumers but still lives inside one of them. The gRPC transport now depends on the SignalR transport's internals for what is really a shared PSU concern (both spawn pwsh, both parse $secret:). Before this PR the two transports were independent siblings. This is the "make X reach into a sibling for a shared capability" shape that's usually better solved by extracting the shared piece: relocate powershell_worker (+ app_token_secret_reference_name) into a neutral module (e.g. psu_powershell, or
a psu parent both transports sit under). Note the worker also returns psu_event_hub::models::WebsocketEventResponse, so a clean extraction should carry/rename that type to something transport-neutral ("Websocket" is already a misnomer for a generic worker response).
Not blocking; happy to submit a follow up PR if you would rather not deal with that here.
|
|
||
| if app_token_secret_reference_name(app_token).is_none() { | ||
| return Ok(Some(app_token.to_owned())); | ||
| } |
There was a problem hiding this comment.
nitpick: This is a redundant $secret: check. PsuGrpcAgent::resolve_app_token checks app_token_secret_reference_name and PowerShellWorker::resolve_app_token checks it again.
Harmless, and this does buy skipping worker construction for literal tokens. Fine to keep; just flagging the double parse in case it’s unintended.
suggestion: A small inline comment would be appreciated if it’s intended 🙂
| @@ -563,9 +563,9 @@ mod tests { | |||
|
|
|||
| #[test] | |||
| fn secret_reference_name_is_case_insensitive() { | |||
There was a problem hiding this comment.
nitpick: Stale test name after the rename.
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Thank you, LGTM!
PSU gRPC agents need to authenticate using configured application tokens while keeping secret-backed tokens out of static configuration. This change resolves the configured gRPC
AppTokenbefore opening the agent stream so literal tokens continue to pass through and$secret:<name>references reuse the existing PSU PowerShell secret resolver.The implementation keeps token handling in the agent transport layer and avoids server-side or per-method parsing in this repository because the ASP.NET Core gRPC server and
PSUPermissionsintegration are not present here. The gRPC request still carries the resolved token through standardauthorizationmetadata for the PSU server authentication middleware to consume.Validation:
cargo +nightly fmt --allcargo test -p devolutions-agent psu_grpc_agent && cargo test -p devolutions-agent psu_event_hub::powershell_worker && cargo test -p devolutions-agent configcargo clippy --workspace --tests -- -D warningsgit --no-pager diff --checkcargo test -p devolutions-agentcargo test --workspacewas attempted. The parallel run hit Windows paging-file/mmap errors; the single-job retry withProgramData=C:\ProgramDataprogressed untildevolutions-agent-updaterrequired elevation (os error 740).