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()); + } }