Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions devolutions-agent/Dockerfile.psu-grpc-poc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ FROM rust:1.90-bookworm AS build

WORKDIR /workspace

ENV CARGO_NET_GIT_FETCH_WITH_CLI=true \
GIT_SSL_NO_VERIFY=true
ENV CARGO_NET_GIT_FETCH_WITH_CLI=true

RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential clang cmake git libclang-dev pkg-config \
Expand All @@ -29,6 +28,11 @@ ENV DAGENT_CONFIG_PATH=/etc/devolutions-agent \
COPY --from=build /workspace/target/release/devolutions-agent /opt/devolutions/agent/devolutions-agent
COPY devolutions-agent/docker/psu-grpc-entrypoint.sh /usr/local/bin/psu-grpc-entrypoint.sh

RUN chmod +x /opt/devolutions/agent/devolutions-agent /usr/local/bin/psu-grpc-entrypoint.sh
RUN useradd --system --create-home --home-dir /var/lib/devolutions-agent --shell /usr/sbin/nologin devolutions-agent \
&& mkdir -p /etc/devolutions-agent \
&& chown -R devolutions-agent:devolutions-agent /etc/devolutions-agent /opt/devolutions/agent \
&& chmod +x /opt/devolutions/agent/devolutions-agent /usr/local/bin/psu-grpc-entrypoint.sh

ENTRYPOINT ["/usr/local/bin/psu-grpc-entrypoint.sh"]
USER devolutions-agent

ENTRYPOINT ["/usr/local/bin/psu-grpc-entrypoint.sh"]
28 changes: 20 additions & 8 deletions devolutions-agent/Run-PsuGrpcAgentContainer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ param(
[string] $ServerUrl = 'http://host.docker.internal:5006',
[string] $AgentId = 'devo-agent-linux',
[string] $DisplayName = 'Devolutions Agent Linux',
[string] $AppToken,
[string[]] $Hubs = @('default'),
[switch] $NoBuild
)
Expand All @@ -24,11 +25,22 @@ if ($existing) {
docker rm -f $ContainerName | Out-Null
}

docker run --rm -it `
--name $ContainerName `
--add-host host.docker.internal:host-gateway `
-e "PSU_SERVER_URL=$ServerUrl" `
-e "PSU_AGENT_ID=$AgentId" `
-e "PSU_DISPLAY_NAME=$DisplayName" `
-e "PSU_HUBS=$($Hubs -join ',')" `
$ImageName
$dockerArgs = @(
'run',
'--rm',
'-it',
'--name', $ContainerName,
'--add-host', 'host.docker.internal:host-gateway',
'-e', "PSU_SERVER_URL=$ServerUrl",
'-e', "PSU_AGENT_ID=$AgentId",
'-e', "PSU_DISPLAY_NAME=$DisplayName",
'-e', "PSU_HUBS=$($Hubs -join ',')"
)

if (-not [string]::IsNullOrWhiteSpace($AppToken)) {
$dockerArgs += @('-e', "PSU_APP_TOKEN=$AppToken")
}

$dockerArgs += $ImageName

docker @dockerArgs
6 changes: 6 additions & 0 deletions devolutions-agent/docker/psu-grpc-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ if [ -z "${hubs_json}" ]; then
hubs_json='"default"'
fi

app_token_property=""
if [ -n "${PSU_APP_TOKEN:-}" ]; then
app_token_property=" \"AppToken\": \"$(json_escape "${PSU_APP_TOKEN}")\","
fi

cat > "${DAGENT_CONFIG_PATH}/agent.json" <<EOF
{
"Updater": {
Expand All @@ -39,6 +44,7 @@ cat > "${DAGENT_CONFIG_PATH}/agent.json" <<EOF
"ServerUrl": "$(json_escape "${PSU_SERVER_URL:-http://host.docker.internal:5006}")",
"AgentId": "$(json_escape "${PSU_AGENT_ID:-devo-agent-linux}")",
"DisplayName": "$(json_escape "${PSU_DISPLAY_NAME:-Devolutions Agent Linux}")",
${app_token_property}
"Hubs": [ ${hubs_json} ],
"PowerShell": {
"ExecutablePath": "$(json_escape "${POWERSHELL_EXECUTABLE:-pwsh}")"
Expand Down
7 changes: 7 additions & 0 deletions devolutions-agent/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,10 @@ pub mod dto {
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,

/// PSU application token used to authenticate the gRPC agent.
#[serde(skip_serializing_if = "Option::is_none")]
pub app_token: Option<String>,

/// Hubs/queues advertised during registration.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hubs: Vec<String>,
Expand All @@ -625,6 +629,7 @@ pub mod dto {
server_url: None,
agent_id: None,
display_name: None,
app_token: None,
hubs: Vec::new(),
powershell: PsuPowerShellConf::default(),
}
Expand Down Expand Up @@ -1002,6 +1007,7 @@ mod tests {
"ServerUrl": "http://localhost:5000",
"AgentId": "agent-01",
"DisplayName": "Agent 01",
"AppToken": "app-token",
"Hubs": ["default"],
"PowerShell": {
"VersionSelector": "7.5"
Expand All @@ -1017,6 +1023,7 @@ mod tests {
"http://localhost:5000/"
);
assert_eq!(conf.psu_grpc_agent.agent_id.as_deref(), Some("agent-01"));
assert_eq!(conf.psu_grpc_agent.app_token.as_deref(), Some("app-token"));
assert_eq!(conf.psu_grpc_agent.powershell.version_selector.as_deref(), Some("7.5"));
}
}
56 changes: 52 additions & 4 deletions devolutions-agent/src/psu_grpc_agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ use anyhow::{Context as _, bail};
use async_trait::async_trait;
use backoff::backoff::Backoff as _;
use devolutions_gateway_task::{ShutdownSignal, Task};
use tokio::sync::{mpsc, oneshot};
use tokio::sync::mpsc;
use tokio::task::JoinSet;
use tokio_stream::wrappers::ReceiverStream;
use tonic::Request;
use tonic::metadata::MetadataValue;
use tonic::transport::Endpoint;
use uuid::Uuid;

use crate::config::{ConfHandle, dto};
use crate::psu_grpc_agent::process::{ProcessControl, ProcessRegistry};

#[allow(unused_qualifications)]
#[allow(unused_qualifications, clippy::clone_on_ref_ptr, clippy::similar_names)]
pub mod protocol {
tonic::include_proto!("devolutions.psu.agent.poc.v1");
}
Expand Down Expand Up @@ -63,6 +64,7 @@ struct PsuGrpcAgent {
agent_id: String,
display_name: String,
machine_name: String,
app_token: Option<String>,
powershell_executable: String,
}

Expand All @@ -76,6 +78,7 @@ impl PsuGrpcAgent {
let machine_name = machine_name();
let agent_id = conf.agent_id.clone().unwrap_or_else(|| machine_name.clone());
let display_name = conf.display_name.clone().unwrap_or_else(|| agent_id.clone());
let app_token = conf.app_token.clone().filter(|token| !token.trim().is_empty());
let powershell_executable = resolve_powershell_executable(&conf.powershell)
.to_string_lossy()
.into_owned();
Expand All @@ -86,6 +89,7 @@ impl PsuGrpcAgent {
agent_id,
display_name,
machine_name,
app_token,
powershell_executable,
})
}
Expand Down Expand Up @@ -151,7 +155,10 @@ impl PsuGrpcAgent {
.context("failed to queue PSU gRPC agent registration")?;

let mut response_stream = client
.connect(Request::new(ReceiverStream::new(outgoing_rx)))
.connect(connect_request(
ReceiverStream::new(outgoing_rx),
self.app_token.as_deref(),
)?)
.await
.context("failed to start PSU gRPC agent stream")?
.into_inner();
Expand Down Expand Up @@ -211,7 +218,7 @@ impl PsuGrpcAgent {
}
Some(ServerPayload::StartProcess(start_process)) => {
let incoming_rx = registry.register_stream(&start_process.stream_id).await;
let (control_tx, control_rx) = oneshot::channel();
let (control_tx, control_rx) = mpsc::channel(8);
registry
.register_process(
start_process.correlation_id.clone(),
Expand Down Expand Up @@ -324,6 +331,20 @@ pub(crate) fn diagnostic(level: &str, message: String) -> AgentDiagnostic {
}
}

fn connect_request<T>(stream: T, app_token: Option<&str>) -> anyhow::Result<Request<T>> {
let mut request = Request::new(stream);

if let Some(token) = app_token {
let authorization = format!("Bearer {token}");
request.metadata_mut().insert(
"authorization",
MetadataValue::try_from(authorization).context("invalid PSU gRPC AppToken metadata")?,
);
}

Ok(request)
}

fn timestamp_now() -> prost_types::Timestamp {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
Expand Down Expand Up @@ -390,3 +411,30 @@ async fn get_powershell_version(executable: &str) -> String {
_ => "unknown".to_owned(),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn connect_request_omits_authorization_without_app_token() {
let request = connect_request((), None).expect("create request");

assert!(!request.metadata().contains_key("authorization"));
}

#[test]
fn connect_request_adds_authorization_with_app_token() {
let request = connect_request((), Some("token")).expect("create request");

assert_eq!(
request
.metadata()
.get("authorization")
.expect("authorization metadata")
.to_str()
.expect("metadata string"),
"Bearer token"
);
}
}
Loading
Loading