From 97f9898fca288edaeb355cb11a88953bef61b94d Mon Sep 17 00:00:00 2001 From: Adam Driscoll Date: Wed, 8 Jul 2026 12:49:13 -0500 Subject: [PATCH] feat(agent): support PSU gRPC app token secrets 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> --- devolutions-agent/src/psu_event_hub/mod.rs | 2 +- .../src/psu_event_hub/powershell_worker.rs | 16 ++--- devolutions-agent/src/psu_grpc_agent/mod.rs | 63 +++++++++++++++++-- 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/devolutions-agent/src/psu_event_hub/mod.rs b/devolutions-agent/src/psu_event_hub/mod.rs index 1a2416209..7722bc8ac 100644 --- a/devolutions-agent/src/psu_event_hub/mod.rs +++ b/devolutions-agent/src/psu_event_hub/mod.rs @@ -1,7 +1,7 @@ pub(crate) mod compat; mod executor; mod models; -mod powershell_worker; +pub(crate) mod powershell_worker; mod result_store; mod signalr; diff --git a/devolutions-agent/src/psu_event_hub/powershell_worker.rs b/devolutions-agent/src/psu_event_hub/powershell_worker.rs index d036c5d9b..4dcdfa89f 100644 --- a/devolutions-agent/src/psu_event_hub/powershell_worker.rs +++ b/devolutions-agent/src/psu_event_hub/powershell_worker.rs @@ -169,7 +169,7 @@ $response | ConvertTo-Json -Compress -Depth 16 const POWERSHELL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30 * 60); #[derive(Debug, Clone)] -pub(super) struct PowerShellWorker { +pub(crate) struct PowerShellWorker { conf: PsuPowerShellConf, permits: Arc, worker_script: Arc, @@ -177,7 +177,7 @@ pub(super) struct PowerShellWorker { } impl PowerShellWorker { - pub(super) fn new(conf: PsuPowerShellConf) -> anyhow::Result { + pub(crate) fn new(conf: PsuPowerShellConf) -> anyhow::Result { Self::with_execution_timeout(conf, POWERSHELL_EXECUTION_TIMEOUT) } @@ -191,8 +191,8 @@ impl PowerShellWorker { }) } - pub(super) async fn resolve_app_token(&self, app_token: &str) -> anyhow::Result { - let Some(secret_name) = secret_reference_name(app_token) else { + pub(crate) async fn resolve_app_token(&self, app_token: &str) -> anyhow::Result { + let Some(secret_name) = app_token_secret_reference_name(app_token) else { return Ok(app_token.to_owned()); }; @@ -399,7 +399,7 @@ impl WorkerRequest { } } -fn secret_reference_name(app_token: &str) -> Option<&str> { +pub(crate) fn app_token_secret_reference_name(app_token: &str) -> Option<&str> { let prefix = "$secret:"; app_token .get(..prefix.len()) @@ -563,9 +563,9 @@ mod tests { #[test] fn secret_reference_name_is_case_insensitive() { - assert_eq!(secret_reference_name("$secret:AppToken"), Some("AppToken")); - assert_eq!(secret_reference_name("$SECRET:AppToken"), Some("AppToken")); - assert_eq!(secret_reference_name("literal-token"), None); + assert_eq!(app_token_secret_reference_name("$secret:AppToken"), Some("AppToken")); + assert_eq!(app_token_secret_reference_name("$SECRET:AppToken"), Some("AppToken")); + assert_eq!(app_token_secret_reference_name("literal-token"), None); } #[test] diff --git a/devolutions-agent/src/psu_grpc_agent/mod.rs b/devolutions-agent/src/psu_grpc_agent/mod.rs index aa1819355..582376dfa 100644 --- a/devolutions-agent/src/psu_grpc_agent/mod.rs +++ b/devolutions-agent/src/psu_grpc_agent/mod.rs @@ -16,6 +16,7 @@ use tonic::transport::Endpoint; use uuid::Uuid; use crate::config::{ConfHandle, dto}; +use crate::psu_event_hub::powershell_worker::{PowerShellWorker, app_token_secret_reference_name}; use crate::psu_grpc_agent::process::{ProcessControl, ProcessRegistry}; #[allow(unused_qualifications, clippy::clone_on_ref_ptr, clippy::similar_names)] @@ -140,6 +141,7 @@ impl PsuGrpcAgent { } 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())?; let channel = endpoint .connect() @@ -155,10 +157,7 @@ impl PsuGrpcAgent { .context("failed to queue PSU gRPC agent registration")?; let mut response_stream = client - .connect(connect_request( - ReceiverStream::new(outgoing_rx), - self.app_token.as_deref(), - )?) + .connect(connect_request(ReceiverStream::new(outgoing_rx), app_token.as_deref())?) .await .context("failed to start PSU gRPC agent stream")? .into_inner(); @@ -259,6 +258,25 @@ impl PsuGrpcAgent { Ok(()) } + async fn resolve_app_token(&self) -> anyhow::Result> { + let Some(app_token) = self.app_token.as_deref() else { + return Ok(None); + }; + + if app_token_secret_reference_name(app_token).is_none() { + return Ok(Some(app_token.to_owned())); + } + + let worker = PowerShellWorker::new(self.conf.powershell.clone()) + .context("failed to initialize PSU PowerShell worker for gRPC AppToken secret resolution")?; + + worker + .resolve_app_token(app_token) + .await + .map(Some) + .context("failed to resolve PSU gRPC AppToken secret") + } + fn create_registration_message(&self, powershell_version: String) -> AgentMessage { AgentMessage { request_id: Uuid::new_v4().simple().to_string(), @@ -437,4 +455,41 @@ mod tests { "Bearer token" ); } + + #[tokio::test] + async fn literal_app_token_does_not_require_secret_resolution() { + let agent = PsuGrpcAgent::new(dto::PsuGrpcAgentConf { + enabled: true, + server_url: Some("http://localhost:5000".parse().expect("server URL")), + agent_id: Some("agent-01".to_owned()), + display_name: None, + app_token: Some("literal-token".to_owned()), + hubs: Vec::new(), + powershell: dto::PsuPowerShellConf { + executable_path: Some("missing-pwsh".into()), + ..dto::PsuPowerShellConf::default() + }, + }) + .expect("create agent"); + + let app_token = agent.resolve_app_token().await.expect("resolve AppToken"); + + assert_eq!(app_token.as_deref(), Some("literal-token")); + } + + #[test] + fn empty_app_token_is_ignored() { + let agent = PsuGrpcAgent::new(dto::PsuGrpcAgentConf { + enabled: true, + server_url: Some("http://localhost:5000".parse().expect("server URL")), + agent_id: Some("agent-01".to_owned()), + display_name: None, + app_token: Some(" ".to_owned()), + hubs: Vec::new(), + powershell: dto::PsuPowerShellConf::default(), + }) + .expect("create agent"); + + assert!(agent.app_token.is_none()); + } }