From 4ebd71c6a38e298deda005f0eb0c98e1b9f7a1de Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 24 Aug 2026 15:08:31 -0400 Subject: [PATCH 1/4] feat(dgw): enable agent tunnel by default Promote the QUIC agent tunnel to a supported Gateway capability. Gateway now starts the listener on UDP 4433 unless explicitly disabled, and enrollment and management APIs no longer require unstable mode. Startup fails if the enabled tunnel cannot initialize or bind. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 8 ++++++ config_schema.json | 22 +++++++++++++++ devolutions-gateway/src/api/mod.rs | 4 +-- devolutions-gateway/src/config.rs | 12 ++++++--- devolutions-gateway/tests/config.rs | 12 +++++++++ .../DevolutionsGateway.psd1 | 2 +- .../DevolutionsGateway/Public/DGateway.ps1 | 27 +++++++++++++++++++ powershell/pester/Config.Tests.ps1 | 12 +++++++++ testsuite/src/dgw_config.rs | 6 ++++- testsuite/tests/cli/agent/tunnel.rs | 2 -- testsuite/tests/cli/agent/up.rs | 1 - 11 files changed, 97 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 70cea9c2e..f9fe9d684 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,14 @@ Stable options are: See the [Cookbook](./docs/COOKBOOK.md) for configuration examples. +- **AgentTunnel** (_Object_): QUIC-based agent tunnel configuration. + The listener is enabled by default and Gateway startup fails if it cannot initialize or bind. + + * **Enabled** (_Boolean_): Whether the agent tunnel listener is enabled (default is `true`). + Set this to `false` to disable the listener. + + * **ListenPort** (_Integer_): UDP port for the QUIC listener (default is `4433`). + - **VerbosityProfile** (_String_): Logging verbosity profile (pre-defined tracing directives). Possible values: diff --git a/config_schema.json b/config_schema.json index 0ffad23f4..18d587dae 100644 --- a/config_schema.json +++ b/config_schema.json @@ -108,6 +108,10 @@ "$ref": "#/definitions/ProxyConf", "description": "HTTP/SOCKS proxy configuration for outbound requests." }, + "AgentTunnel": { + "$ref": "#/definitions/AgentTunnelConf", + "description": "QUIC-based agent tunnel configuration." + }, "LogFile": { "type": "string", "description": "Path to the log file." @@ -508,6 +512,24 @@ }, "additionalProperties": false }, + "AgentTunnelConf": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean", + "default": true, + "description": "Whether the agent tunnel listener is enabled." + }, + "ListenPort": { + "type": "integer", + "minimum": 0, + "maximum": 65535, + "default": 4433, + "description": "UDP port for the QUIC listener." + } + }, + "additionalProperties": false + }, "DebugConf": { "type": "object", "properties": { diff --git a/devolutions-gateway/src/api/mod.rs b/devolutions-gateway/src/api/mod.rs index 325a37d83..c7b222ebc 100644 --- a/devolutions-gateway/src/api/mod.rs +++ b/devolutions-gateway/src/api/mod.rs @@ -35,7 +35,8 @@ pub fn make_router(state: crate::DgwState) -> axum::Router { .nest("/jet/webapp", webapp::make_router(state.clone())) .nest("/jet/net", net::make_router(state.clone())) .nest("/jet/traffic", traffic::make_router(state.clone())) - .nest("/jet/update", update::make_router(state.clone())); + .nest("/jet/update", update::make_router(state.clone())) + .nest("/jet/tunnel", tunnel::make_router(state.clone())); if state.conf_handle.get_conf().web_app.enabled { router = router.route( @@ -45,7 +46,6 @@ pub fn make_router(state: crate::DgwState) -> axum::Router { } if state.conf_handle.get_conf().debug.enable_unstable { - router = router.nest("/jet/tunnel", tunnel::make_router(state.clone())); router = router.nest("/jet/net/monitor", monitoring::make_router(state.clone())); } diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index f5d0de0a1..75c69ccd7 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -1252,7 +1252,7 @@ pub mod dto { #[serde(skip_serializing_if = "Option::is_none")] pub proxy: Option, - /// (Unstable) Agent tunnel configuration (QUIC-based agent tunnel) + /// QUIC-based agent tunnel configuration #[serde(skip_serializing_if = "Option::is_none")] pub agent_tunnel: Option, @@ -1351,12 +1351,12 @@ pub mod dto { } } - /// (Unstable) QUIC-based agent tunnel configuration + /// QUIC-based agent tunnel configuration #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct AgentTunnelConf { /// Whether the agent tunnel listener is enabled - #[serde(default)] + #[serde(default = "AgentTunnelConf::default_enabled")] pub enabled: bool, /// UDP port for the QUIC listener (default: 4433) #[serde(default = "AgentTunnelConf::default_listen_port")] @@ -1364,6 +1364,10 @@ pub mod dto { } impl AgentTunnelConf { + fn default_enabled() -> bool { + true + } + fn default_listen_port() -> u16 { 4433 } @@ -1372,7 +1376,7 @@ pub mod dto { impl Default for AgentTunnelConf { fn default() -> Self { Self { - enabled: false, + enabled: Self::default_enabled(), listen_port: Self::default_listen_port(), } } diff --git a/devolutions-gateway/tests/config.rs b/devolutions-gateway/tests/config.rs index 8af342523..0383ca284 100644 --- a/devolutions-gateway/tests/config.rs +++ b/devolutions-gateway/tests/config.rs @@ -467,3 +467,15 @@ fn sample_parsing(#[case] sample: Sample) { assert_eq!(from_json, from_struct); } + +#[rstest] +#[case(r#"{"Listeners":[]}"#, true)] +#[case(r#"{"Listeners":[],"AgentTunnel":{}}"#, true)] +#[case(r#"{"Listeners":[],"AgentTunnel":{"Enabled":false}}"#, false)] +fn agent_tunnel_enabled_by_default(#[case] json: &str, #[case] expected_enabled: bool) { + let conf_file = serde_json::from_str::(json).unwrap(); + let agent_tunnel = conf_file.agent_tunnel.unwrap_or_default(); + + assert_eq!(agent_tunnel.enabled, expected_enabled); + assert_eq!(agent_tunnel.listen_port, 4433); +} diff --git a/powershell/DevolutionsGateway/DevolutionsGateway.psd1 b/powershell/DevolutionsGateway/DevolutionsGateway.psd1 index f97bcd833..a5e65b468 100644 --- a/powershell/DevolutionsGateway/DevolutionsGateway.psd1 +++ b/powershell/DevolutionsGateway/DevolutionsGateway.psd1 @@ -76,7 +76,7 @@ 'New-DGatewayProvisionerKeyPair', 'Import-DGatewayProvisionerKey', 'New-DGatewayDelegationKeyPair', 'Import-DGatewayDelegationKey', 'New-DGatewayToken', - 'New-DGatewayWebAppConfig', + 'New-DGatewayWebAppConfig', 'New-DGatewayAgentTunnelConfig', 'Set-DGatewayUser', 'Remove-DGatewayUser', 'Get-DGatewayUser', 'Start-DGateway', 'Stop-DGateway', 'Restart-DGateway', 'Get-DGatewayVersion', 'Get-DGatewayPackage', diff --git a/powershell/DevolutionsGateway/Public/DGateway.ps1 b/powershell/DevolutionsGateway/Public/DGateway.ps1 index a08b1e93b..5950a60cf 100644 --- a/powershell/DevolutionsGateway/Public/DGateway.ps1 +++ b/powershell/DevolutionsGateway/Public/DGateway.ps1 @@ -281,6 +281,29 @@ function New-DGatewayWebAppConfig() { $webapp } +class DGatewayAgentTunnelConfig { + [bool] $Enabled + [System.UInt16] $ListenPort + + DGatewayAgentTunnelConfig() { } + + DGatewayAgentTunnelConfig([bool] $Enabled, [System.UInt16] $ListenPort) { + $this.Enabled = $Enabled + $this.ListenPort = $ListenPort + } +} + +function New-DGatewayAgentTunnelConfig() { + [CmdletBinding()] + [OutputType('DGatewayAgentTunnelConfig')] + param( + [bool] $Enabled = $true, + [System.UInt16] $ListenPort = 4433 + ) + + [DGatewayAgentTunnelConfig]::new($Enabled, $ListenPort) +} + enum VerbosityProfile { Default Debug @@ -317,6 +340,8 @@ class DGatewayConfig { [DGatewayWebAppConfig] $WebApp + [DGatewayAgentTunnelConfig] $AgentTunnel + [string] $LogDirective [string] $VerbosityProfile } @@ -397,6 +422,8 @@ function Set-DGatewayConfig { [DGatewayWebAppConfig] $WebApp, + [DGatewayAgentTunnelConfig] $AgentTunnel, + [VerbosityProfile] $VerbosityProfile ) diff --git a/powershell/pester/Config.Tests.ps1 b/powershell/pester/Config.Tests.ps1 index 640a0dabf..7eef473f0 100644 --- a/powershell/pester/Config.Tests.ps1 +++ b/powershell/pester/Config.Tests.ps1 @@ -136,6 +136,18 @@ Describe 'Devolutions Gateway config' { $(Get-DGatewayConfig -ConfigPath:$ConfigPath).WebApp.LoginLimitRate | Should -Be 8 } + It 'Sets agent tunnel configuration' { + $AgentTunnel = New-DGatewayAgentTunnelConfig -ListenPort 8443 + Set-DGatewayConfig -ConfigPath:$ConfigPath -AgentTunnel $AgentTunnel + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.Enabled | Should -Be $true + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.ListenPort | Should -Be 8443 + + $AgentTunnel = New-DGatewayAgentTunnelConfig -Enabled $false -ListenPort 9443 + Set-DGatewayConfig -ConfigPath:$ConfigPath -AgentTunnel $AgentTunnel + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.Enabled | Should -Be $false + $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.ListenPort | Should -Be 9443 + } + It 'Sets basic standalone configuration' { $Hostname = "localhost" $HttpListener = New-DGatewayListener 'http://*:7172' 'http://*:7172' diff --git a/testsuite/src/dgw_config.rs b/testsuite/src/dgw_config.rs index eb8baa0de..bce92db2d 100644 --- a/testsuite/src/dgw_config.rs +++ b/testsuite/src/dgw_config.rs @@ -134,7 +134,11 @@ impl DgwConfigHandle { at_config.enabled ) } else { - String::new() + r#", + "AgentTunnel": { + "Enabled": false + }"# + .to_owned() }; let hostname_json = hostname diff --git a/testsuite/tests/cli/agent/tunnel.rs b/testsuite/tests/cli/agent/tunnel.rs index c0856a54e..c24851fb4 100644 --- a/testsuite/tests/cli/agent/tunnel.rs +++ b/testsuite/tests/cli/agent/tunnel.rs @@ -580,7 +580,6 @@ async fn enrolled_agent_forwards_domain_only_route_and_reconnects() { .hostname("localhost".to_owned()) .provisioner_public_key_data(public_key_data) .agent_tunnel(AgentTunnelConfig::builder().build()) - .enable_unstable(true) .build() .init() .expect("initialize gateway config"); @@ -668,7 +667,6 @@ async fn docker_isolates_real_agent_dns_and_ip_routes() { .listener_host("0.0.0.0") .provisioner_public_key_data(public_key_data) .agent_tunnel(AgentTunnelConfig::builder().build()) - .enable_unstable(true) .build() .init() .expect("initialize gateway config"); diff --git a/testsuite/tests/cli/agent/up.rs b/testsuite/tests/cli/agent/up.rs index 8e0303d2f..5d16b00ea 100644 --- a/testsuite/tests/cli/agent/up.rs +++ b/testsuite/tests/cli/agent/up.rs @@ -81,7 +81,6 @@ async fn up_enrollment_against_real_gateway() { let config_handle = DgwConfig::builder() .disable_token_validation(true) .agent_tunnel(AgentTunnelConfig::builder().build()) - .enable_unstable(true) .build() .init() .expect("init gateway config"); From c7701959d0dafa6970a0ef707672ad9d5542b547 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 24 Aug 2026 18:02:37 -0400 Subject: [PATCH 2/4] fix(dgw): remove agent tunnel enable switch Agent Tunnel is now always initialized by Gateway and its configuration only selects the UDP listen port. Test Gateway instances use ephemeral UDP ports so the always-on listener does not introduce parallel test collisions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 5 +- config_schema.json | 5 -- devolutions-gateway/src/config.rs | 8 --- devolutions-gateway/src/service.rs | 51 ++++++++----------- devolutions-gateway/tests/config.rs | 11 ++-- .../DevolutionsGateway/Public/DGateway.ps1 | 7 +-- powershell/pester/Config.Tests.ps1 | 4 +- testsuite/src/dgw_config.rs | 37 +++----------- testsuite/tests/cli/agent/tunnel.rs | 4 +- testsuite/tests/cli/agent/up.rs | 3 +- 10 files changed, 39 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index f9fe9d684..65fadd77c 100644 --- a/README.md +++ b/README.md @@ -279,10 +279,7 @@ Stable options are: See the [Cookbook](./docs/COOKBOOK.md) for configuration examples. - **AgentTunnel** (_Object_): QUIC-based agent tunnel configuration. - The listener is enabled by default and Gateway startup fails if it cannot initialize or bind. - - * **Enabled** (_Boolean_): Whether the agent tunnel listener is enabled (default is `true`). - Set this to `false` to disable the listener. + Gateway always starts the listener and fails startup if it cannot initialize or bind. * **ListenPort** (_Integer_): UDP port for the QUIC listener (default is `4433`). diff --git a/config_schema.json b/config_schema.json index 18d587dae..9df28cab7 100644 --- a/config_schema.json +++ b/config_schema.json @@ -515,11 +515,6 @@ "AgentTunnelConf": { "type": "object", "properties": { - "Enabled": { - "type": "boolean", - "default": true, - "description": "Whether the agent tunnel listener is enabled." - }, "ListenPort": { "type": "integer", "minimum": 0, diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index 75c69ccd7..02a2148eb 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -1355,19 +1355,12 @@ pub mod dto { #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct AgentTunnelConf { - /// Whether the agent tunnel listener is enabled - #[serde(default = "AgentTunnelConf::default_enabled")] - pub enabled: bool, /// UDP port for the QUIC listener (default: 4433) #[serde(default = "AgentTunnelConf::default_listen_port")] pub listen_port: u16, } impl AgentTunnelConf { - fn default_enabled() -> bool { - true - } - fn default_listen_port() -> u16 { 4433 } @@ -1376,7 +1369,6 @@ pub mod dto { impl Default for AgentTunnelConf { fn default() -> Self { Self { - enabled: Self::default_enabled(), listen_port: Self::default_listen_port(), } } diff --git a/devolutions-gateway/src/service.rs b/devolutions-gateway/src/service.rs index 6602c9746..bac984088 100644 --- a/devolutions-gateway/src/service.rs +++ b/devolutions-gateway/src/service.rs @@ -274,37 +274,30 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { ); let monitoring_state = Arc::new(network_monitor::State::new(Arc::new(filesystem_monitor_config_cache))?); - // Initialize agent tunnel if configured. - let agent_tunnel_handle = if conf.agent_tunnel.enabled { - let data_dir = config::get_data_dir(); - let hostname = &conf.hostname; - - let ca_manager = agent_tunnel::cert::CaManager::load_or_generate(&data_dir) - .context("failed to initialize agent tunnel CA")?; - - // Bind to the IPv6 unspecified address so the listener is dual-stack and - // accepts both IPv4 and IPv6 agent connections (matters when an agent's DNS - // resolution returns an IPv6 address for the configured gateway endpoint). - // The listener crate explicitly clears `IPV6_V6ONLY` for portability across - // OSes, and falls back to IPv4 if the host has IPv6 disabled. - let listen_addr = std::net::SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, conf.agent_tunnel.listen_port)); - - let (listener, handle) = - agent_tunnel::AgentTunnelListener::bind(listen_addr, Arc::clone(&ca_manager), hostname) - .await - .context("failed to bind agent tunnel listener")?; + let data_dir = config::get_data_dir(); + let hostname = &conf.hostname; - tasks.register(listener); + let ca_manager = + agent_tunnel::cert::CaManager::load_or_generate(&data_dir).context("failed to initialize agent tunnel CA")?; - info!( - port = conf.agent_tunnel.listen_port, - "Agent tunnel QUIC listener started", - ); + // Bind to the IPv6 unspecified address so the listener is dual-stack and + // accepts both IPv4 and IPv6 agent connections (matters when an agent's DNS + // resolution returns an IPv6 address for the configured gateway endpoint). + // The listener crate explicitly clears `IPV6_V6ONLY` for portability across + // OSes, and falls back to IPv4 if the host has IPv6 disabled. + let listen_addr = std::net::SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, conf.agent_tunnel.listen_port)); - Some(Arc::new(handle)) - } else { - None - }; + let (agent_tunnel_listener, agent_tunnel_handle) = + agent_tunnel::AgentTunnelListener::bind(listen_addr, Arc::clone(&ca_manager), hostname) + .await + .context("failed to bind agent tunnel listener")?; + + tasks.register(agent_tunnel_listener); + + info!( + port = conf.agent_tunnel.listen_port, + "Agent tunnel QUIC listener started", + ); let state = DgwState { conf_handle: conf_handle.clone(), @@ -318,7 +311,7 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { credentials: credentials.clone(), monitoring_state, traffic_audit_handle: traffic_audit_task.handle(), - agent_tunnel_handle, + agent_tunnel_handle: Some(Arc::new(agent_tunnel_handle)), }; for listener in &conf.listeners { diff --git a/devolutions-gateway/tests/config.rs b/devolutions-gateway/tests/config.rs index 0383ca284..d6b14e404 100644 --- a/devolutions-gateway/tests/config.rs +++ b/devolutions-gateway/tests/config.rs @@ -469,13 +469,12 @@ fn sample_parsing(#[case] sample: Sample) { } #[rstest] -#[case(r#"{"Listeners":[]}"#, true)] -#[case(r#"{"Listeners":[],"AgentTunnel":{}}"#, true)] -#[case(r#"{"Listeners":[],"AgentTunnel":{"Enabled":false}}"#, false)] -fn agent_tunnel_enabled_by_default(#[case] json: &str, #[case] expected_enabled: bool) { +#[case(r#"{"Listeners":[]}"#, 4433)] +#[case(r#"{"Listeners":[],"AgentTunnel":{}}"#, 4433)] +#[case(r#"{"Listeners":[],"AgentTunnel":{"ListenPort":8443}}"#, 8443)] +fn agent_tunnel_listen_port(#[case] json: &str, #[case] expected_listen_port: u16) { let conf_file = serde_json::from_str::(json).unwrap(); let agent_tunnel = conf_file.agent_tunnel.unwrap_or_default(); - assert_eq!(agent_tunnel.enabled, expected_enabled); - assert_eq!(agent_tunnel.listen_port, 4433); + assert_eq!(agent_tunnel.listen_port, expected_listen_port); } diff --git a/powershell/DevolutionsGateway/Public/DGateway.ps1 b/powershell/DevolutionsGateway/Public/DGateway.ps1 index 5950a60cf..9ea9b4d32 100644 --- a/powershell/DevolutionsGateway/Public/DGateway.ps1 +++ b/powershell/DevolutionsGateway/Public/DGateway.ps1 @@ -282,13 +282,11 @@ function New-DGatewayWebAppConfig() { } class DGatewayAgentTunnelConfig { - [bool] $Enabled [System.UInt16] $ListenPort DGatewayAgentTunnelConfig() { } - DGatewayAgentTunnelConfig([bool] $Enabled, [System.UInt16] $ListenPort) { - $this.Enabled = $Enabled + DGatewayAgentTunnelConfig([System.UInt16] $ListenPort) { $this.ListenPort = $ListenPort } } @@ -297,11 +295,10 @@ function New-DGatewayAgentTunnelConfig() { [CmdletBinding()] [OutputType('DGatewayAgentTunnelConfig')] param( - [bool] $Enabled = $true, [System.UInt16] $ListenPort = 4433 ) - [DGatewayAgentTunnelConfig]::new($Enabled, $ListenPort) + [DGatewayAgentTunnelConfig]::new($ListenPort) } enum VerbosityProfile { diff --git a/powershell/pester/Config.Tests.ps1 b/powershell/pester/Config.Tests.ps1 index 7eef473f0..6963547c1 100644 --- a/powershell/pester/Config.Tests.ps1 +++ b/powershell/pester/Config.Tests.ps1 @@ -139,12 +139,10 @@ Describe 'Devolutions Gateway config' { It 'Sets agent tunnel configuration' { $AgentTunnel = New-DGatewayAgentTunnelConfig -ListenPort 8443 Set-DGatewayConfig -ConfigPath:$ConfigPath -AgentTunnel $AgentTunnel - $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.Enabled | Should -Be $true $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.ListenPort | Should -Be 8443 - $AgentTunnel = New-DGatewayAgentTunnelConfig -Enabled $false -ListenPort 9443 + $AgentTunnel = New-DGatewayAgentTunnelConfig -ListenPort 9443 Set-DGatewayConfig -ConfigPath:$ConfigPath -AgentTunnel $AgentTunnel - $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.Enabled | Should -Be $false $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.ListenPort | Should -Be 9443 } diff --git a/testsuite/src/dgw_config.rs b/testsuite/src/dgw_config.rs index bce92db2d..ede6fe2d6 100644 --- a/testsuite/src/dgw_config.rs +++ b/testsuite/src/dgw_config.rs @@ -21,17 +21,6 @@ impl fmt::Display for VerbosityProfile { } } -/// Configuration for the agent tunnel feature in tests. -#[derive(Clone, TypedBuilder)] -pub struct AgentTunnelConfig { - /// Whether the agent tunnel is enabled. - #[builder(default = true)] - pub enabled: bool, - /// UDP port for the QUIC listener. - #[builder(default, setter(into))] - pub listen_port: Option, -} - #[derive(TypedBuilder)] pub struct DgwConfig { #[builder(default, setter(into))] @@ -57,9 +46,6 @@ pub struct DgwConfig { /// Pass a path that does not yet exist to test behaviour before the folder is created. #[builder(default, setter(into))] recording_path: Option, - /// Agent tunnel (QUIC) configuration. - #[builder(default, setter(into))] - agent_tunnel: Option, } fn find_unused_port() -> u16 { @@ -103,7 +89,6 @@ impl DgwConfigHandle { verbosity_profile, enable_unstable, recording_path, - agent_tunnel, } = config; let tempdir = tempfile::tempdir().context("create tempdir")?; @@ -123,23 +108,13 @@ impl DgwConfigHandle { String::new() }; - let agent_tunnel_json = if let Some(at_config) = agent_tunnel { - let listen_port = at_config.listen_port.unwrap_or_else(find_unused_udp_port); - format!( - r#", - "AgentTunnel": {{ - "Enabled": {}, - "ListenPort": {listen_port} - }}"#, - at_config.enabled - ) - } else { + let agent_tunnel_port = find_unused_udp_port(); + let agent_tunnel_json = format!( r#", - "AgentTunnel": { - "Enabled": false - }"# - .to_owned() - }; + "AgentTunnel": {{ + "ListenPort": {agent_tunnel_port} + }}"# + ); let hostname_json = hostname .map(|hostname| { diff --git a/testsuite/tests/cli/agent/tunnel.rs b/testsuite/tests/cli/agent/tunnel.rs index c24851fb4..8516ae360 100644 --- a/testsuite/tests/cli/agent/tunnel.rs +++ b/testsuite/tests/cli/agent/tunnel.rs @@ -18,7 +18,7 @@ use picky::jose::jwt::CheckedJwtSig; use picky::key::PrivateKey; use serde::Serialize; use testsuite::cli::{agent_assert_cmd, agent_tokio_cmd, dgw_tokio_cmd, wait_for_tcp_port}; -use testsuite::dgw_config::{AgentTunnelConfig, DgwConfig}; +use testsuite::dgw_config::DgwConfig; use tokio::net::TcpListener; use tokio::process::Child; use tokio_tungstenite::tungstenite::Message; @@ -579,7 +579,6 @@ async fn enrolled_agent_forwards_domain_only_route_and_reconnects() { let config = DgwConfig::builder() .hostname("localhost".to_owned()) .provisioner_public_key_data(public_key_data) - .agent_tunnel(AgentTunnelConfig::builder().build()) .build() .init() .expect("initialize gateway config"); @@ -666,7 +665,6 @@ async fn docker_isolates_real_agent_dns_and_ip_routes() { .hostname(DOCKER_GATEWAY_HOST.to_owned()) .listener_host("0.0.0.0") .provisioner_public_key_data(public_key_data) - .agent_tunnel(AgentTunnelConfig::builder().build()) .build() .init() .expect("initialize gateway config"); diff --git a/testsuite/tests/cli/agent/up.rs b/testsuite/tests/cli/agent/up.rs index 5d16b00ea..1d36d344e 100644 --- a/testsuite/tests/cli/agent/up.rs +++ b/testsuite/tests/cli/agent/up.rs @@ -76,11 +76,10 @@ fn up_enrollment_string_stdin_empty_is_error() { async fn up_enrollment_against_real_gateway() { use anyhow::Context as _; use testsuite::cli::{agent_assert_cmd, dgw_tokio_cmd, wait_for_tcp_port}; - use testsuite::dgw_config::{AgentTunnelConfig, DgwConfig}; + use testsuite::dgw_config::DgwConfig; let config_handle = DgwConfig::builder() .disable_token_validation(true) - .agent_tunnel(AgentTunnelConfig::builder().build()) .build() .init() .expect("init gateway config"); From 7901585af2837f8996a3ec044811f5de37603089 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 24 Aug 2026 18:21:22 -0400 Subject: [PATCH 3/4] fix(dgw): reject zero agent tunnel port Keep the configured endpoint consistent with the UDP listener by requiring a nonzero port across Rust, the JSON schema, and PowerShell. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- config_schema.json | 2 +- devolutions-gateway/src/config.rs | 6 +++--- devolutions-gateway/src/service.rs | 5 +++-- devolutions-gateway/tests/config.rs | 9 ++++++++- powershell/DevolutionsGateway/Public/DGateway.ps1 | 2 ++ powershell/pester/Config.Tests.ps1 | 2 ++ 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/config_schema.json b/config_schema.json index 9df28cab7..70124be7a 100644 --- a/config_schema.json +++ b/config_schema.json @@ -517,7 +517,7 @@ "properties": { "ListenPort": { "type": "integer", - "minimum": 0, + "minimum": 1, "maximum": 65535, "default": 4433, "description": "UDP port for the QUIC listener." diff --git a/devolutions-gateway/src/config.rs b/devolutions-gateway/src/config.rs index 02a2148eb..b55c6e0ad 100644 --- a/devolutions-gateway/src/config.rs +++ b/devolutions-gateway/src/config.rs @@ -1357,12 +1357,12 @@ pub mod dto { pub struct AgentTunnelConf { /// UDP port for the QUIC listener (default: 4433) #[serde(default = "AgentTunnelConf::default_listen_port")] - pub listen_port: u16, + pub listen_port: std::num::NonZeroU16, } impl AgentTunnelConf { - fn default_listen_port() -> u16 { - 4433 + fn default_listen_port() -> std::num::NonZeroU16 { + std::num::NonZeroU16::new(4433).expect("default port is non-zero") } } diff --git a/devolutions-gateway/src/service.rs b/devolutions-gateway/src/service.rs index bac984088..94eef63c3 100644 --- a/devolutions-gateway/src/service.rs +++ b/devolutions-gateway/src/service.rs @@ -285,7 +285,8 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { // resolution returns an IPv6 address for the configured gateway endpoint). // The listener crate explicitly clears `IPV6_V6ONLY` for portability across // OSes, and falls back to IPv4 if the host has IPv6 disabled. - let listen_addr = std::net::SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, conf.agent_tunnel.listen_port)); + let listen_addr = + std::net::SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, conf.agent_tunnel.listen_port.get())); let (agent_tunnel_listener, agent_tunnel_handle) = agent_tunnel::AgentTunnelListener::bind(listen_addr, Arc::clone(&ca_manager), hostname) @@ -295,7 +296,7 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { tasks.register(agent_tunnel_listener); info!( - port = conf.agent_tunnel.listen_port, + port = conf.agent_tunnel.listen_port.get(), "Agent tunnel QUIC listener started", ); diff --git a/devolutions-gateway/tests/config.rs b/devolutions-gateway/tests/config.rs index d6b14e404..417b95c65 100644 --- a/devolutions-gateway/tests/config.rs +++ b/devolutions-gateway/tests/config.rs @@ -476,5 +476,12 @@ fn agent_tunnel_listen_port(#[case] json: &str, #[case] expected_listen_port: u1 let conf_file = serde_json::from_str::(json).unwrap(); let agent_tunnel = conf_file.agent_tunnel.unwrap_or_default(); - assert_eq!(agent_tunnel.listen_port, expected_listen_port); + assert_eq!(agent_tunnel.listen_port.get(), expected_listen_port); +} + +#[test] +fn agent_tunnel_zero_port_is_rejected() { + let result = serde_json::from_str::(r#"{"Listeners":[],"AgentTunnel":{"ListenPort":0}}"#); + + assert!(result.is_err()); } diff --git a/powershell/DevolutionsGateway/Public/DGateway.ps1 b/powershell/DevolutionsGateway/Public/DGateway.ps1 index 9ea9b4d32..4b0cb1b82 100644 --- a/powershell/DevolutionsGateway/Public/DGateway.ps1 +++ b/powershell/DevolutionsGateway/Public/DGateway.ps1 @@ -282,6 +282,7 @@ function New-DGatewayWebAppConfig() { } class DGatewayAgentTunnelConfig { + [ValidateRange(1, 65535)] [System.UInt16] $ListenPort DGatewayAgentTunnelConfig() { } @@ -295,6 +296,7 @@ function New-DGatewayAgentTunnelConfig() { [CmdletBinding()] [OutputType('DGatewayAgentTunnelConfig')] param( + [ValidateRange(1, 65535)] [System.UInt16] $ListenPort = 4433 ) diff --git a/powershell/pester/Config.Tests.ps1 b/powershell/pester/Config.Tests.ps1 index 6963547c1..4ca2dc046 100644 --- a/powershell/pester/Config.Tests.ps1 +++ b/powershell/pester/Config.Tests.ps1 @@ -144,6 +144,8 @@ Describe 'Devolutions Gateway config' { $AgentTunnel = New-DGatewayAgentTunnelConfig -ListenPort 9443 Set-DGatewayConfig -ConfigPath:$ConfigPath -AgentTunnel $AgentTunnel $(Get-DGatewayConfig -ConfigPath:$ConfigPath).AgentTunnel.ListenPort | Should -Be 9443 + + { New-DGatewayAgentTunnelConfig -ListenPort 0 } | Should -Throw } It 'Sets basic standalone configuration' { From 373b65239ae3db2a606b597ae81744d1bde5bd32 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 24 Aug 2026 19:48:56 -0400 Subject: [PATCH 4/4] feat(dgw): publish agent tunnel API Add the stable Agent Tunnel enrollment and management endpoints to the Gateway OpenAPI contract and regenerate the .NET and TypeScript clients. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- devolutions-gateway/openapi/doc/index.adoc | 572 +++++++++++ .../dotnet-client/.openapi-generator/FILES | 10 + .../openapi/dotnet-client/README.md | 23 +- .../openapi/dotnet-client/docs/AgentApi.md | 403 ++++++++ .../docs/AgentDomainAdvertisement.md | 11 + .../openapi/dotnet-client/docs/AgentInfo.md | 17 + .../dotnet-client/docs/EnrollRequest.md | 12 + .../dotnet-client/docs/EnrollResponse.md | 14 + .../Api/AgentApi.cs | 893 ++++++++++++++++++ .../Model/AgentDomainAdvertisement.cs | 103 ++ .../Model/AgentInfo.cs | 184 ++++ .../Model/EnrollRequest.cs | 115 +++ .../Model/EnrollResponse.cs | 150 +++ devolutions-gateway/openapi/gateway-api.yaml | 215 +++++ .../.openapi-generator/FILES | 5 + .../ts-angular-client/api/agent.service.ts | 387 ++++++++ .../openapi/ts-angular-client/api/api.ts | 4 +- .../ts-angular-client/configuration.ts | 9 + .../model/agentDomainAdvertisement.ts | 16 + .../ts-angular-client/model/agentInfo.ts | 23 + .../ts-angular-client/model/enrollRequest.ts | 26 + .../ts-angular-client/model/enrollResponse.ts | 34 + .../openapi/ts-angular-client/model/models.ts | 4 + devolutions-gateway/src/api/tunnel.rs | 128 ++- devolutions-gateway/src/openapi.rs | 19 + 25 files changed, 3363 insertions(+), 14 deletions(-) create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/AgentApi.md create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/AgentDomainAdvertisement.md create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/AgentInfo.md create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/EnrollRequest.md create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/EnrollResponse.md create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/AgentApi.cs create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentDomainAdvertisement.cs create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentInfo.cs create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EnrollRequest.cs create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EnrollResponse.cs create mode 100644 devolutions-gateway/openapi/ts-angular-client/api/agent.service.ts create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/agentDomainAdvertisement.ts create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/agentInfo.ts create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/enrollRequest.ts create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/enrollResponse.ts diff --git a/devolutions-gateway/openapi/doc/index.adoc b/devolutions-gateway/openapi/doc/index.adoc index 6910ffa17..5156f9b8d 100644 --- a/devolutions-gateway/openapi/doc/index.adoc +++ b/devolutions-gateway/openapi/doc/index.adoc @@ -23,6 +23,11 @@ Protocol-aware fine-grained relay server == Access +* *Bearer* Authentication `enrollment_token` + + + + * *Bearer* Authentication `jrec_token` @@ -57,6 +62,387 @@ Protocol-aware fine-grained relay server == Endpoints +[.Agent] +=== Agent + + +[.deleteAgent] +==== deleteAgent + +`DELETE /jet/tunnel/agents/{agent_id}` + +Delete (unregister) an agent by ID. + +===== Description + + + + +// markup not found, no include::{specDir}jet/tunnel/agents/\{agent_id\}/DELETE/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `scope_token` +| http +| bearer +|=== + +===== Parameters + +====== Path Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| agent_id +| Agent ID +| X +| null +| + +|=== + + + + + + +===== Return Type + + + +- + + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 204 +| Agent deleted +| <<>> + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + + +| 404 +| Agent not found +| <<>> + + +| 500 +| Unexpected server error +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/tunnel/agents/\{agent_id\}/DELETE/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.enrollAgent] +==== enrollAgent + +`POST /jet/tunnel/enroll` + +Enroll a new agent. + +===== Description + +Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + + +// markup not found, no include::{specDir}jet/tunnel/enroll/POST/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `enrollment_token` +| http +| bearer +|=== + +===== Parameters + + +====== Body Parameter + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| EnrollRequest +| Agent identity and certificate signing request <> +| X +| +| + +|=== + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Agent enrolled +| <> + + +| 400 +| Invalid agent name, request body, or certificate signing request +| <<>> + + +| 401 +| Invalid or missing enrollment token +| <<>> + + +| 409 +| Agent ID already registered +| <<>> + + +| 500 +| Unexpected server error +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/tunnel/enroll/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getAgent] +==== getAgent + +`GET /jet/tunnel/agents/{agent_id}` + +Get a single agent by ID. + +===== Description + + + + +// markup not found, no include::{specDir}jet/tunnel/agents/\{agent_id\}/GET/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `scope_token` +| http +| bearer +|=== + +===== Parameters + +====== Path Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| agent_id +| Agent ID +| X +| null +| + +|=== + + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Agent status +| <> + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + + +| 404 +| Agent not found +| <<>> + + +| 500 +| Unexpected server error +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/tunnel/agents/\{agent_id\}/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.listAgents] +==== listAgents + +`GET /jet/tunnel/agents` + +List connected agents and their status. + +===== Description + + + + +// markup not found, no include::{specDir}jet/tunnel/agents/GET/spec.adoc[opts=optional] + + + +===== Security + +[cols="2,1,1"] +|=== +| Name | Type | Scheme + +| `scope_token` +| http +| bearer +|=== + + +===== Return Type + +array[<>] + + +===== Content Type + +* application/json + +===== Responses + +.HTTP Response Codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| Connected agents +| List[<>] + + +| 401 +| Invalid or missing authorization token +| <<>> + + +| 403 +| Insufficient permissions +| <<>> + + +| 500 +| Unexpected server error +| <<>> + +|=== + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jet/tunnel/agents/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + [.Config] === Config @@ -3052,6 +3438,106 @@ endif::internal-generation[] |=== +[#AgentDomainAdvertisement] +=== _AgentDomainAdvertisement_ + + + + +[.fields-AgentDomainAdvertisement] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| auto_detected +| X +| +| Boolean +| +| + +| domain +| X +| +| String +| +| + +|=== + + + +[#AgentInfo] +=== _AgentInfo_ + + + + +[.fields-AgentInfo] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| agent_id +| X +| +| UUID +| +| uuid + +| cert_fingerprint +| X +| +| String +| +| + +| domains +| X +| +| List of <> +| +| + +| is_online +| X +| +| Boolean +| +| + +| last_seen_ms +| X +| +| Long +| +| int64 + +| name +| X +| +| String +| +| + +| route_epoch +| X +| +| Long +| +| int64 + +| subnets +| X +| +| List of <> +| +| + +|=== + + + [#AppCredential] === _AppCredential_ @@ -3437,6 +3923,92 @@ Service configuration diagnostic +[#EnrollRequest] +=== _EnrollRequest_ + + + + +[.fields-EnrollRequest] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| agent_hostname +| +| X +| String +| Optional hostname of the agent machine (added as DNS SAN in the issued certificate). +| + +| agent_id +| X +| +| UUID +| Agent-generated UUID (the agent owns its identity). +| uuid + +| csr_pem +| X +| +| String +| PEM-encoded Certificate Signing Request from the agent. +| + +|=== + + + +[#EnrollResponse] +=== _EnrollResponse_ + + + + +[.fields-EnrollResponse] +[cols="2,1,1,2,4,1"] +|=== +| Field Name| Required| Nullable | Type| Description | Format + +| agent_id +| X +| +| UUID +| Assigned agent ID. +| uuid + +| client_cert_pem +| X +| +| String +| PEM-encoded client certificate (signed by the gateway CA). +| + +| gateway_ca_cert_pem +| X +| +| String +| PEM-encoded gateway CA certificate (for server verification). +| + +| quic_endpoint +| X +| +| String +| QUIC endpoint to connect to (`host:port`). +| + +| server_spki_sha256 +| X +| +| String +| SHA-256 hash of the server certificate's SPKI (hex-encoded). Used by the agent to pin the server's public key. +| + +|=== + + + [#EventOutcomeResponse] === _EventOutcomeResponse_ diff --git a/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES b/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES index 90efa2314..0968dae76 100644 --- a/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES +++ b/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES @@ -5,6 +5,9 @@ docs/AccessScope.md docs/AckRequest.md docs/AckResponse.md docs/AddressFamily.md +docs/AgentApi.md +docs/AgentDomainAdvertisement.md +docs/AgentInfo.md docs/AppCredential.md docs/AppCredentialKind.md docs/AppTokenContentType.md @@ -18,6 +21,8 @@ docs/ConnectionMode.md docs/DataEncoding.md docs/DeleteManyResult.md docs/DiagnosticsApi.md +docs/EnrollRequest.md +docs/EnrollResponse.md docs/EventOutcomeResponse.md docs/GetUpdateProductsResponse.md docs/GetUpdateScheduleResponse.md @@ -71,6 +76,7 @@ docs/UpdateApi.md docs/UpdateProductInfo.md docs/UpdateRequestSchema.md docs/WebAppApi.md +src/Devolutions.Gateway.Client/Api/AgentApi.cs src/Devolutions.Gateway.Client/Api/ConfigApi.cs src/Devolutions.Gateway.Client/Api/DiagnosticsApi.cs src/Devolutions.Gateway.Client/Api/HealthApi.cs @@ -107,6 +113,8 @@ src/Devolutions.Gateway.Client/Model/AccessScope.cs src/Devolutions.Gateway.Client/Model/AckRequest.cs src/Devolutions.Gateway.Client/Model/AckResponse.cs src/Devolutions.Gateway.Client/Model/AddressFamily.cs +src/Devolutions.Gateway.Client/Model/AgentDomainAdvertisement.cs +src/Devolutions.Gateway.Client/Model/AgentInfo.cs src/Devolutions.Gateway.Client/Model/AppCredential.cs src/Devolutions.Gateway.Client/Model/AppCredentialKind.cs src/Devolutions.Gateway.Client/Model/AppTokenContentType.cs @@ -118,6 +126,8 @@ src/Devolutions.Gateway.Client/Model/ConfigPatch.cs src/Devolutions.Gateway.Client/Model/ConnectionMode.cs src/Devolutions.Gateway.Client/Model/DataEncoding.cs src/Devolutions.Gateway.Client/Model/DeleteManyResult.cs +src/Devolutions.Gateway.Client/Model/EnrollRequest.cs +src/Devolutions.Gateway.Client/Model/EnrollResponse.cs src/Devolutions.Gateway.Client/Model/EventOutcomeResponse.cs src/Devolutions.Gateway.Client/Model/GetUpdateProductsResponse.cs src/Devolutions.Gateway.Client/Model/GetUpdateScheduleResponse.cs diff --git a/devolutions-gateway/openapi/dotnet-client/README.md b/devolutions-gateway/openapi/dotnet-client/README.md index c77d0ee47..4ac6f60c1 100644 --- a/devolutions-gateway/openapi/dotnet-client/README.md +++ b/devolutions-gateway/openapi/dotnet-client/README.md @@ -114,17 +114,17 @@ namespace Example // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes HttpClient httpClient = new HttpClient(); HttpClientHandler httpClientHandler = new HttpClientHandler(); - var apiInstance = new ConfigApi(httpClient, config, httpClientHandler); - var configPatch = new ConfigPatch(); // ConfigPatch | JSON-encoded configuration patch + var apiInstance = new AgentApi(httpClient, config, httpClientHandler); + var agentId = "agentId_example"; // Guid | Agent ID try { - // Modifies configuration - apiInstance.PatchConfig(configPatch); + // Delete (unregister) an agent by ID. + apiInstance.DeleteAgent(agentId); } catch (ApiException e) { - Debug.Print("Exception when calling ConfigApi.PatchConfig: " + e.Message ); + Debug.Print("Exception when calling AgentApi.DeleteAgent: " + e.Message ); Debug.Print("Status Code: "+ e.ErrorCode); Debug.Print(e.StackTrace); } @@ -141,6 +141,10 @@ All URIs are relative to *http://localhost* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- +*AgentApi* | [**DeleteAgent**](docs/AgentApi.md#deleteagent) | **DELETE** /jet/tunnel/agents/{agent_id} | Delete (unregister) an agent by ID. +*AgentApi* | [**EnrollAgent**](docs/AgentApi.md#enrollagent) | **POST** /jet/tunnel/enroll | Enroll a new agent. +*AgentApi* | [**GetAgent**](docs/AgentApi.md#getagent) | **GET** /jet/tunnel/agents/{agent_id} | Get a single agent by ID. +*AgentApi* | [**ListAgents**](docs/AgentApi.md#listagents) | **GET** /jet/tunnel/agents | List connected agents and their status. *ConfigApi* | [**PatchConfig**](docs/ConfigApi.md#patchconfig) | **PATCH** /jet/config | Modifies configuration *DiagnosticsApi* | [**GetClockDiagnostic**](docs/DiagnosticsApi.md#getclockdiagnostic) | **GET** /jet/diagnostics/clock | Retrieves server's clock in order to diagnose clock drifting. *DiagnosticsApi* | [**GetConfigurationDiagnostic**](docs/DiagnosticsApi.md#getconfigurationdiagnostic) | **GET** /jet/diagnostics/configuration | Retrieves a subset of the configuration, for diagnosis purposes. @@ -179,6 +183,8 @@ Class | Method | HTTP request | Description - [Model.AckRequest](docs/AckRequest.md) - [Model.AckResponse](docs/AckResponse.md) - [Model.AddressFamily](docs/AddressFamily.md) + - [Model.AgentDomainAdvertisement](docs/AgentDomainAdvertisement.md) + - [Model.AgentInfo](docs/AgentInfo.md) - [Model.AppCredential](docs/AppCredential.md) - [Model.AppCredentialKind](docs/AppCredentialKind.md) - [Model.AppTokenContentType](docs/AppTokenContentType.md) @@ -190,6 +196,8 @@ Class | Method | HTTP request | Description - [Model.ConnectionMode](docs/ConnectionMode.md) - [Model.DataEncoding](docs/DataEncoding.md) - [Model.DeleteManyResult](docs/DeleteManyResult.md) + - [Model.EnrollRequest](docs/EnrollRequest.md) + - [Model.EnrollResponse](docs/EnrollResponse.md) - [Model.EventOutcomeResponse](docs/EventOutcomeResponse.md) - [Model.GetUpdateProductsResponse](docs/GetUpdateProductsResponse.md) - [Model.GetUpdateScheduleResponse](docs/GetUpdateScheduleResponse.md) @@ -239,6 +247,11 @@ Class | Method | HTTP request | Description Authentication schemes defined for the API: + +### enrollment_token + +- **Type**: Bearer Authentication + ### jrec_token diff --git a/devolutions-gateway/openapi/dotnet-client/docs/AgentApi.md b/devolutions-gateway/openapi/dotnet-client/docs/AgentApi.md new file mode 100644 index 000000000..bab458b00 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/AgentApi.md @@ -0,0 +1,403 @@ +# Devolutions.Gateway.Client.Api.AgentApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|--------|--------------|-------------| +| [**DeleteAgent**](AgentApi.md#deleteagent) | **DELETE** /jet/tunnel/agents/{agent_id} | Delete (unregister) an agent by ID. | +| [**EnrollAgent**](AgentApi.md#enrollagent) | **POST** /jet/tunnel/enroll | Enroll a new agent. | +| [**GetAgent**](AgentApi.md#getagent) | **GET** /jet/tunnel/agents/{agent_id} | Get a single agent by ID. | +| [**ListAgents**](AgentApi.md#listagents) | **GET** /jet/tunnel/agents | List connected agents and their status. | + + +# **DeleteAgent** +> void DeleteAgent (Guid agentId) + +Delete (unregister) an agent by ID. + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class DeleteAgentExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: scope_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new AgentApi(httpClient, config, httpClientHandler); + var agentId = "agentId_example"; // Guid | Agent ID + + try + { + // Delete (unregister) an agent by ID. + apiInstance.DeleteAgent(agentId); + } + catch (ApiException e) + { + Debug.Print("Exception when calling AgentApi.DeleteAgent: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the DeleteAgentWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Delete (unregister) an agent by ID. + apiInstance.DeleteAgentWithHttpInfo(agentId); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling AgentApi.DeleteAgentWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters + +| Name | Type | Description | Notes | +|------|------|-------------|-------| +| **agentId** | **Guid** | Agent ID | | + +### Return type + +void (empty response body) + +### Authorization + +[scope_token](../README.md#scope_token) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **204** | Agent deleted | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | +| **404** | Agent not found | - | +| **500** | Unexpected server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +# **EnrollAgent** +> EnrollResponse EnrollAgent (EnrollRequest enrollRequest) + +Enroll a new agent. + +Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class EnrollAgentExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: enrollment_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new AgentApi(httpClient, config, httpClientHandler); + var enrollRequest = new EnrollRequest(); // EnrollRequest | Agent identity and certificate signing request + + try + { + // Enroll a new agent. + EnrollResponse result = apiInstance.EnrollAgent(enrollRequest); + Debug.WriteLine(result); + } + catch (ApiException e) + { + Debug.Print("Exception when calling AgentApi.EnrollAgent: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the EnrollAgentWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Enroll a new agent. + ApiResponse response = apiInstance.EnrollAgentWithHttpInfo(enrollRequest); + Debug.Write("Status Code: " + response.StatusCode); + Debug.Write("Response Headers: " + response.Headers); + Debug.Write("Response Body: " + response.Data); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling AgentApi.EnrollAgentWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters + +| Name | Type | Description | Notes | +|------|------|-------------|-------| +| **enrollRequest** | [**EnrollRequest**](EnrollRequest.md) | Agent identity and certificate signing request | | + +### Return type + +[**EnrollResponse**](EnrollResponse.md) + +### Authorization + +[enrollment_token](../README.md#enrollment_token) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Agent enrolled | - | +| **400** | Invalid agent name, request body, or certificate signing request | - | +| **401** | Invalid or missing enrollment token | - | +| **409** | Agent ID already registered | - | +| **500** | Unexpected server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +# **GetAgent** +> AgentInfo GetAgent (Guid agentId) + +Get a single agent by ID. + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class GetAgentExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: scope_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new AgentApi(httpClient, config, httpClientHandler); + var agentId = "agentId_example"; // Guid | Agent ID + + try + { + // Get a single agent by ID. + AgentInfo result = apiInstance.GetAgent(agentId); + Debug.WriteLine(result); + } + catch (ApiException e) + { + Debug.Print("Exception when calling AgentApi.GetAgent: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the GetAgentWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // Get a single agent by ID. + ApiResponse response = apiInstance.GetAgentWithHttpInfo(agentId); + Debug.Write("Status Code: " + response.StatusCode); + Debug.Write("Response Headers: " + response.Headers); + Debug.Write("Response Body: " + response.Data); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling AgentApi.GetAgentWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters + +| Name | Type | Description | Notes | +|------|------|-------------|-------| +| **agentId** | **Guid** | Agent ID | | + +### Return type + +[**AgentInfo**](AgentInfo.md) + +### Authorization + +[scope_token](../README.md#scope_token) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Agent status | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | +| **404** | Agent not found | - | +| **500** | Unexpected server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +# **ListAgents** +> List<AgentInfo> ListAgents () + +List connected agents and their status. + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using Devolutions.Gateway.Client.Api; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Example +{ + public class ListAgentsExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://localhost"; + // Configure Bearer token for authorization: scope_token + config.AccessToken = "YOUR_BEARER_TOKEN"; + + // create instances of HttpClient, HttpClientHandler to be reused later with different Api classes + HttpClient httpClient = new HttpClient(); + HttpClientHandler httpClientHandler = new HttpClientHandler(); + var apiInstance = new AgentApi(httpClient, config, httpClientHandler); + + try + { + // List connected agents and their status. + List result = apiInstance.ListAgents(); + Debug.WriteLine(result); + } + catch (ApiException e) + { + Debug.Print("Exception when calling AgentApi.ListAgents: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the ListAgentsWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + // List connected agents and their status. + ApiResponse> response = apiInstance.ListAgentsWithHttpInfo(); + Debug.Write("Status Code: " + response.StatusCode); + Debug.Write("Response Headers: " + response.Headers); + Debug.Write("Response Body: " + response.Data); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling AgentApi.ListAgentsWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters +This endpoint does not need any parameter. +### Return type + +[**List<AgentInfo>**](AgentInfo.md) + +### Authorization + +[scope_token](../README.md#scope_token) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Connected agents | - | +| **401** | Invalid or missing authorization token | - | +| **403** | Insufficient permissions | - | +| **500** | Unexpected server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/AgentDomainAdvertisement.md b/devolutions-gateway/openapi/dotnet-client/docs/AgentDomainAdvertisement.md new file mode 100644 index 000000000..9127b7807 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/AgentDomainAdvertisement.md @@ -0,0 +1,11 @@ +# Devolutions.Gateway.Client.Model.AgentDomainAdvertisement + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**AutoDetected** | **bool** | | +**Domain** | **string** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/AgentInfo.md b/devolutions-gateway/openapi/dotnet-client/docs/AgentInfo.md new file mode 100644 index 000000000..dba2fc646 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/AgentInfo.md @@ -0,0 +1,17 @@ +# Devolutions.Gateway.Client.Model.AgentInfo + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**AgentId** | **Guid** | | +**CertFingerprint** | **string** | | +**Domains** | [**List<AgentDomainAdvertisement>**](AgentDomainAdvertisement.md) | | +**IsOnline** | **bool** | | +**LastSeenMs** | **long** | | +**Name** | **string** | | +**RouteEpoch** | **long** | | +**Subnets** | **List<string>** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/EnrollRequest.md b/devolutions-gateway/openapi/dotnet-client/docs/EnrollRequest.md new file mode 100644 index 000000000..9fdfb80b3 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/EnrollRequest.md @@ -0,0 +1,12 @@ +# Devolutions.Gateway.Client.Model.EnrollRequest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**AgentHostname** | **string** | Optional hostname of the agent machine (added as DNS SAN in the issued certificate). | [optional] +**AgentId** | **Guid** | Agent-generated UUID (the agent owns its identity). | +**CsrPem** | **string** | PEM-encoded Certificate Signing Request from the agent. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/docs/EnrollResponse.md b/devolutions-gateway/openapi/dotnet-client/docs/EnrollResponse.md new file mode 100644 index 000000000..e739404e5 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/EnrollResponse.md @@ -0,0 +1,14 @@ +# Devolutions.Gateway.Client.Model.EnrollResponse + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**AgentId** | **Guid** | Assigned agent ID. | +**ClientCertPem** | **string** | PEM-encoded client certificate (signed by the gateway CA). | +**GatewayCaCertPem** | **string** | PEM-encoded gateway CA certificate (for server verification). | +**QuicEndpoint** | **string** | QUIC endpoint to connect to (`host:port`). | +**ServerSpkiSha256** | **string** | SHA-256 hash of the server certificate's SPKI (hex-encoded). Used by the agent to pin the server's public key. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/AgentApi.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/AgentApi.cs new file mode 100644 index 000000000..1775f446f --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Api/AgentApi.cs @@ -0,0 +1,893 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Mime; +using Devolutions.Gateway.Client.Client; +using Devolutions.Gateway.Client.Model; + +namespace Devolutions.Gateway.Client.Api +{ + + /// + /// Represents a collection of functions to interact with the API endpoints + /// + public interface IAgentApiSync : IApiAccessor + { + #region Synchronous Operations + /// + /// Delete (unregister) an agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// + void DeleteAgent(Guid agentId); + + /// + /// Delete (unregister) an agent by ID. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Agent ID + /// ApiResponse of Object(void) + ApiResponse DeleteAgentWithHttpInfo(Guid agentId); + /// + /// Enroll a new agent. + /// + /// + /// Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + /// + /// Thrown when fails to make API call + /// Agent identity and certificate signing request + /// EnrollResponse + EnrollResponse EnrollAgent(EnrollRequest enrollRequest); + + /// + /// Enroll a new agent. + /// + /// + /// Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + /// + /// Thrown when fails to make API call + /// Agent identity and certificate signing request + /// ApiResponse of EnrollResponse + ApiResponse EnrollAgentWithHttpInfo(EnrollRequest enrollRequest); + /// + /// Get a single agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// AgentInfo + AgentInfo GetAgent(Guid agentId); + + /// + /// Get a single agent by ID. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Agent ID + /// ApiResponse of AgentInfo + ApiResponse GetAgentWithHttpInfo(Guid agentId); + /// + /// List connected agents and their status. + /// + /// Thrown when fails to make API call + /// List<AgentInfo> + List ListAgents(); + + /// + /// List connected agents and their status. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// ApiResponse of List<AgentInfo> + ApiResponse> ListAgentsWithHttpInfo(); + #endregion Synchronous Operations + } + + /// + /// Represents a collection of functions to interact with the API endpoints + /// + public interface IAgentApiAsync : IApiAccessor + { + #region Asynchronous Operations + /// + /// Delete (unregister) an agent by ID. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Agent ID + /// Cancellation Token to cancel the request. + /// Task of void + System.Threading.Tasks.Task DeleteAgentAsync(Guid agentId, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Delete (unregister) an agent by ID. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Agent ID + /// Cancellation Token to cancel the request. + /// Task of ApiResponse + System.Threading.Tasks.Task> DeleteAgentWithHttpInfoAsync(Guid agentId, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// Enroll a new agent. + /// + /// + /// Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + /// + /// Thrown when fails to make API call + /// Agent identity and certificate signing request + /// Cancellation Token to cancel the request. + /// Task of EnrollResponse + System.Threading.Tasks.Task EnrollAgentAsync(EnrollRequest enrollRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Enroll a new agent. + /// + /// + /// Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + /// + /// Thrown when fails to make API call + /// Agent identity and certificate signing request + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (EnrollResponse) + System.Threading.Tasks.Task> EnrollAgentWithHttpInfoAsync(EnrollRequest enrollRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// Get a single agent by ID. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Agent ID + /// Cancellation Token to cancel the request. + /// Task of AgentInfo + System.Threading.Tasks.Task GetAgentAsync(Guid agentId, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// Get a single agent by ID. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Agent ID + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (AgentInfo) + System.Threading.Tasks.Task> GetAgentWithHttpInfoAsync(Guid agentId, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + /// + /// List connected agents and their status. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of List<AgentInfo> + System.Threading.Tasks.Task> ListAgentsAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + + /// + /// List connected agents and their status. + /// + /// + /// + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (List<AgentInfo>) + System.Threading.Tasks.Task>> ListAgentsWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)); + #endregion Asynchronous Operations + } + + /// + /// Represents a collection of functions to interact with the API endpoints + /// + public interface IAgentApi : IAgentApiSync, IAgentApiAsync + { + + } + + /// + /// Represents a collection of functions to interact with the API endpoints + /// + public partial class AgentApi : IDisposable, IAgentApi + { + private Devolutions.Gateway.Client.Client.ExceptionFactory _exceptionFactory = (name, response) => null; + + /// + /// Initializes a new instance of the class. + /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. + /// It's better to reuse the HttpClient and HttpClientHandler. + /// + /// + public AgentApi() : this((string)null) + { + } + + /// + /// Initializes a new instance of the class. + /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. + /// It's better to reuse the HttpClient and HttpClientHandler. + /// + /// The target service's base path in URL format. + /// + /// + public AgentApi(string basePath) + { + this.Configuration = Devolutions.Gateway.Client.Client.Configuration.MergeConfigurations( + Devolutions.Gateway.Client.Client.GlobalConfiguration.Instance, + new Devolutions.Gateway.Client.Client.Configuration { BasePath = basePath } + ); + this.ApiClient = new Devolutions.Gateway.Client.Client.ApiClient(this.Configuration.BasePath); + this.Client = this.ApiClient; + this.AsynchronousClient = this.ApiClient; + this.ExceptionFactory = Devolutions.Gateway.Client.Client.Configuration.DefaultExceptionFactory; + } + + /// + /// Initializes a new instance of the class using Configuration object. + /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. + /// It's better to reuse the HttpClient and HttpClientHandler. + /// + /// An instance of Configuration. + /// + /// + public AgentApi(Devolutions.Gateway.Client.Client.Configuration configuration) + { + if (configuration == null) throw new ArgumentNullException("configuration"); + + this.Configuration = Devolutions.Gateway.Client.Client.Configuration.MergeConfigurations( + Devolutions.Gateway.Client.Client.GlobalConfiguration.Instance, + configuration + ); + this.ApiClient = new Devolutions.Gateway.Client.Client.ApiClient(this.Configuration.BasePath); + this.Client = this.ApiClient; + this.AsynchronousClient = this.ApiClient; + ExceptionFactory = Devolutions.Gateway.Client.Client.Configuration.DefaultExceptionFactory; + } + + /// + /// Initializes a new instance of the class. + /// + /// An instance of HttpClient. + /// An optional instance of HttpClientHandler that is used by HttpClient. + /// + /// + /// + /// Some configuration settings will not be applied without passing an HttpClientHandler. + /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. + /// + public AgentApi(HttpClient client, HttpClientHandler handler = null) : this(client, (string)null, handler) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// An instance of HttpClient. + /// The target service's base path in URL format. + /// An optional instance of HttpClientHandler that is used by HttpClient. + /// + /// + /// + /// + /// Some configuration settings will not be applied without passing an HttpClientHandler. + /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. + /// + public AgentApi(HttpClient client, string basePath, HttpClientHandler handler = null) + { + if (client == null) throw new ArgumentNullException("client"); + + this.Configuration = Devolutions.Gateway.Client.Client.Configuration.MergeConfigurations( + Devolutions.Gateway.Client.Client.GlobalConfiguration.Instance, + new Devolutions.Gateway.Client.Client.Configuration { BasePath = basePath } + ); + this.ApiClient = new Devolutions.Gateway.Client.Client.ApiClient(client, this.Configuration.BasePath, handler); + this.Client = this.ApiClient; + this.AsynchronousClient = this.ApiClient; + this.ExceptionFactory = Devolutions.Gateway.Client.Client.Configuration.DefaultExceptionFactory; + } + + /// + /// Initializes a new instance of the class using Configuration object. + /// + /// An instance of HttpClient. + /// An instance of Configuration. + /// An optional instance of HttpClientHandler that is used by HttpClient. + /// + /// + /// + /// Some configuration settings will not be applied without passing an HttpClientHandler. + /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. + /// + public AgentApi(HttpClient client, Devolutions.Gateway.Client.Client.Configuration configuration, HttpClientHandler handler = null) + { + if (configuration == null) throw new ArgumentNullException("configuration"); + if (client == null) throw new ArgumentNullException("client"); + + this.Configuration = Devolutions.Gateway.Client.Client.Configuration.MergeConfigurations( + Devolutions.Gateway.Client.Client.GlobalConfiguration.Instance, + configuration + ); + this.ApiClient = new Devolutions.Gateway.Client.Client.ApiClient(client, this.Configuration.BasePath, handler); + this.Client = this.ApiClient; + this.AsynchronousClient = this.ApiClient; + ExceptionFactory = Devolutions.Gateway.Client.Client.Configuration.DefaultExceptionFactory; + } + + /// + /// Initializes a new instance of the class + /// using a Configuration object and client instance. + /// + /// The client interface for synchronous API access. + /// The client interface for asynchronous API access. + /// The configuration object. + /// + public AgentApi(Devolutions.Gateway.Client.Client.ISynchronousClient client, Devolutions.Gateway.Client.Client.IAsynchronousClient asyncClient, Devolutions.Gateway.Client.Client.IReadableConfiguration configuration) + { + if (client == null) throw new ArgumentNullException("client"); + if (asyncClient == null) throw new ArgumentNullException("asyncClient"); + if (configuration == null) throw new ArgumentNullException("configuration"); + + this.Client = client; + this.AsynchronousClient = asyncClient; + this.Configuration = configuration; + this.ExceptionFactory = Devolutions.Gateway.Client.Client.Configuration.DefaultExceptionFactory; + } + + /// + /// Disposes resources if they were created by us + /// + public void Dispose() + { + this.ApiClient?.Dispose(); + } + + /// + /// Holds the ApiClient if created + /// + public Devolutions.Gateway.Client.Client.ApiClient ApiClient { get; set; } = null; + + /// + /// The client for accessing this underlying API asynchronously. + /// + public Devolutions.Gateway.Client.Client.IAsynchronousClient AsynchronousClient { get; set; } + + /// + /// The client for accessing this underlying API synchronously. + /// + public Devolutions.Gateway.Client.Client.ISynchronousClient Client { get; set; } + + /// + /// Gets the base path of the API client. + /// + /// The base path + public string GetBasePath() + { + return this.Configuration.BasePath; + } + + /// + /// Gets or sets the configuration object + /// + /// An instance of the Configuration + public Devolutions.Gateway.Client.Client.IReadableConfiguration Configuration { get; set; } + + /// + /// Provides a factory method hook for the creation of exceptions. + /// + public Devolutions.Gateway.Client.Client.ExceptionFactory ExceptionFactory + { + get + { + if (_exceptionFactory != null && _exceptionFactory.GetInvocationList().Length > 1) + { + throw new InvalidOperationException("Multicast delegate for ExceptionFactory is unsupported."); + } + return _exceptionFactory; + } + set { _exceptionFactory = value; } + } + + /// + /// Delete (unregister) an agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// + public void DeleteAgent(Guid agentId) + { + DeleteAgentWithHttpInfo(agentId); + } + + /// + /// Delete (unregister) an agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// ApiResponse of Object(void) + public Devolutions.Gateway.Client.Client.ApiResponse DeleteAgentWithHttpInfo(Guid agentId) + { + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.PathParameters.Add("agent_id", Devolutions.Gateway.Client.Client.ClientUtils.ParameterToString(agentId)); // path parameter + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Delete("/jet/tunnel/agents/{agent_id}", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("DeleteAgent", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Delete (unregister) an agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// Cancellation Token to cancel the request. + /// Task of void + public async System.Threading.Tasks.Task DeleteAgentAsync(Guid agentId, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + await DeleteAgentWithHttpInfoAsync(agentId, cancellationToken).ConfigureAwait(false); + } + + /// + /// Delete (unregister) an agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// Cancellation Token to cancel the request. + /// Task of ApiResponse + public async System.Threading.Tasks.Task> DeleteAgentWithHttpInfoAsync(Guid agentId, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.PathParameters.Add("agent_id", Devolutions.Gateway.Client.Client.ClientUtils.ParameterToString(agentId)); // path parameter + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.DeleteAsync("/jet/tunnel/agents/{agent_id}", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("DeleteAgent", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Enroll a new agent. Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + /// + /// Thrown when fails to make API call + /// Agent identity and certificate signing request + /// EnrollResponse + public EnrollResponse EnrollAgent(EnrollRequest enrollRequest) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = EnrollAgentWithHttpInfo(enrollRequest); + return localVarResponse.Data; + } + + /// + /// Enroll a new agent. Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + /// + /// Thrown when fails to make API call + /// Agent identity and certificate signing request + /// ApiResponse of EnrollResponse + public Devolutions.Gateway.Client.Client.ApiResponse EnrollAgentWithHttpInfo(EnrollRequest enrollRequest) + { + // verify the required parameter 'enrollRequest' is set + if (enrollRequest == null) + throw new Devolutions.Gateway.Client.Client.ApiException(400, "Missing required parameter 'enrollRequest' when calling AgentApi->EnrollAgent"); + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + "application/json" + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.Data = enrollRequest; + + // authentication (enrollment_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Post("/jet/tunnel/enroll", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("EnrollAgent", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Enroll a new agent. Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + /// + /// Thrown when fails to make API call + /// Agent identity and certificate signing request + /// Cancellation Token to cancel the request. + /// Task of EnrollResponse + public async System.Threading.Tasks.Task EnrollAgentAsync(EnrollRequest enrollRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await EnrollAgentWithHttpInfoAsync(enrollRequest, cancellationToken).ConfigureAwait(false); + return localVarResponse.Data; + } + + /// + /// Enroll a new agent. Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + /// + /// Thrown when fails to make API call + /// Agent identity and certificate signing request + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (EnrollResponse) + public async System.Threading.Tasks.Task> EnrollAgentWithHttpInfoAsync(EnrollRequest enrollRequest, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + // verify the required parameter 'enrollRequest' is set + if (enrollRequest == null) + throw new Devolutions.Gateway.Client.Client.ApiException(400, "Missing required parameter 'enrollRequest' when calling AgentApi->EnrollAgent"); + + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + "application/json" + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.Data = enrollRequest; + + // authentication (enrollment_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.PostAsync("/jet/tunnel/enroll", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("EnrollAgent", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Get a single agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// AgentInfo + public AgentInfo GetAgent(Guid agentId) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = GetAgentWithHttpInfo(agentId); + return localVarResponse.Data; + } + + /// + /// Get a single agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// ApiResponse of AgentInfo + public Devolutions.Gateway.Client.Client.ApiResponse GetAgentWithHttpInfo(Guid agentId) + { + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.PathParameters.Add("agent_id", Devolutions.Gateway.Client.Client.ClientUtils.ParameterToString(agentId)); // path parameter + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Get("/jet/tunnel/agents/{agent_id}", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetAgent", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// Get a single agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// Cancellation Token to cancel the request. + /// Task of AgentInfo + public async System.Threading.Tasks.Task GetAgentAsync(Guid agentId, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + Devolutions.Gateway.Client.Client.ApiResponse localVarResponse = await GetAgentWithHttpInfoAsync(agentId, cancellationToken).ConfigureAwait(false); + return localVarResponse.Data; + } + + /// + /// Get a single agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (AgentInfo) + public async System.Threading.Tasks.Task> GetAgentWithHttpInfoAsync(Guid agentId, System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + localVarRequestOptions.PathParameters.Add("agent_id", Devolutions.Gateway.Client.Client.ClientUtils.ParameterToString(agentId)); // path parameter + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.GetAsync("/jet/tunnel/agents/{agent_id}", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("GetAgent", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// List connected agents and their status. + /// + /// Thrown when fails to make API call + /// List<AgentInfo> + public List ListAgents() + { + Devolutions.Gateway.Client.Client.ApiResponse> localVarResponse = ListAgentsWithHttpInfo(); + return localVarResponse.Data; + } + + /// + /// List connected agents and their status. + /// + /// Thrown when fails to make API call + /// ApiResponse of List<AgentInfo> + public Devolutions.Gateway.Client.Client.ApiResponse> ListAgentsWithHttpInfo() + { + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + var localVarResponse = this.Client.Get>("/jet/tunnel/agents", localVarRequestOptions, this.Configuration); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("ListAgents", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + /// + /// List connected agents and their status. + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of List<AgentInfo> + public async System.Threading.Tasks.Task> ListAgentsAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + Devolutions.Gateway.Client.Client.ApiResponse> localVarResponse = await ListAgentsWithHttpInfoAsync(cancellationToken).ConfigureAwait(false); + return localVarResponse.Data; + } + + /// + /// List connected agents and their status. + /// + /// Thrown when fails to make API call + /// Cancellation Token to cancel the request. + /// Task of ApiResponse (List<AgentInfo>) + public async System.Threading.Tasks.Task>> ListAgentsWithHttpInfoAsync(System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + { + + Devolutions.Gateway.Client.Client.RequestOptions localVarRequestOptions = new Devolutions.Gateway.Client.Client.RequestOptions(); + + string[] _contentTypes = new string[] { + }; + + // to determine the Accept header + string[] _accepts = new string[] { + "application/json" + }; + + + var localVarContentType = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderContentType(_contentTypes); + if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); + + var localVarAccept = Devolutions.Gateway.Client.Client.ClientUtils.SelectHeaderAccept(_accepts); + if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); + + + // authentication (scope_token) required + // bearer authentication required + if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) + { + localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); + } + + // make the HTTP request + + var localVarResponse = await this.AsynchronousClient.GetAsync>("/jet/tunnel/agents", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); + + if (this.ExceptionFactory != null) + { + Exception _exception = this.ExceptionFactory("ListAgents", localVarResponse); + if (_exception != null) throw _exception; + } + + return localVarResponse; + } + + } +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentDomainAdvertisement.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentDomainAdvertisement.cs new file mode 100644 index 000000000..4391bf48e --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentDomainAdvertisement.cs @@ -0,0 +1,103 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// AgentDomainAdvertisement + /// + [DataContract(Name = "AgentDomainAdvertisement")] + public partial class AgentDomainAdvertisement : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected AgentDomainAdvertisement() { } + /// + /// Initializes a new instance of the class. + /// + /// autoDetected (required). + /// domain (required). + public AgentDomainAdvertisement(bool autoDetected = default(bool), string domain = default(string)) + { + this.AutoDetected = autoDetected; + // to ensure "domain" is required (not null) + if (domain == null) + { + throw new ArgumentNullException("domain is a required property for AgentDomainAdvertisement and cannot be null"); + } + this.Domain = domain; + } + + /// + /// Gets or Sets AutoDetected + /// + [DataMember(Name = "auto_detected", IsRequired = true, EmitDefaultValue = true)] + public bool AutoDetected { get; set; } + + /// + /// Gets or Sets Domain + /// + [DataMember(Name = "domain", IsRequired = true, EmitDefaultValue = true)] + public string Domain { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class AgentDomainAdvertisement {\n"); + sb.Append(" AutoDetected: ").Append(AutoDetected).Append("\n"); + sb.Append(" Domain: ").Append(Domain).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentInfo.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentInfo.cs new file mode 100644 index 000000000..91492363a --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentInfo.cs @@ -0,0 +1,184 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// AgentInfo + /// + [DataContract(Name = "AgentInfo")] + public partial class AgentInfo : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected AgentInfo() { } + /// + /// Initializes a new instance of the class. + /// + /// agentId (required). + /// certFingerprint (required). + /// domains (required). + /// isOnline (required). + /// lastSeenMs (required). + /// name (required). + /// routeEpoch (required). + /// subnets (required). + public AgentInfo(Guid agentId = default(Guid), string certFingerprint = default(string), List domains = default(List), bool isOnline = default(bool), long lastSeenMs = default(long), string name = default(string), long routeEpoch = default(long), List subnets = default(List)) + { + this.AgentId = agentId; + // to ensure "certFingerprint" is required (not null) + if (certFingerprint == null) + { + throw new ArgumentNullException("certFingerprint is a required property for AgentInfo and cannot be null"); + } + this.CertFingerprint = certFingerprint; + // to ensure "domains" is required (not null) + if (domains == null) + { + throw new ArgumentNullException("domains is a required property for AgentInfo and cannot be null"); + } + this.Domains = domains; + this.IsOnline = isOnline; + this.LastSeenMs = lastSeenMs; + // to ensure "name" is required (not null) + if (name == null) + { + throw new ArgumentNullException("name is a required property for AgentInfo and cannot be null"); + } + this.Name = name; + this.RouteEpoch = routeEpoch; + // to ensure "subnets" is required (not null) + if (subnets == null) + { + throw new ArgumentNullException("subnets is a required property for AgentInfo and cannot be null"); + } + this.Subnets = subnets; + } + + /// + /// Gets or Sets AgentId + /// + [DataMember(Name = "agent_id", IsRequired = true, EmitDefaultValue = true)] + public Guid AgentId { get; set; } + + /// + /// Gets or Sets CertFingerprint + /// + [DataMember(Name = "cert_fingerprint", IsRequired = true, EmitDefaultValue = true)] + public string CertFingerprint { get; set; } + + /// + /// Gets or Sets Domains + /// + [DataMember(Name = "domains", IsRequired = true, EmitDefaultValue = true)] + public List Domains { get; set; } + + /// + /// Gets or Sets IsOnline + /// + [DataMember(Name = "is_online", IsRequired = true, EmitDefaultValue = true)] + public bool IsOnline { get; set; } + + /// + /// Gets or Sets LastSeenMs + /// + [DataMember(Name = "last_seen_ms", IsRequired = true, EmitDefaultValue = true)] + public long LastSeenMs { get; set; } + + /// + /// Gets or Sets Name + /// + [DataMember(Name = "name", IsRequired = true, EmitDefaultValue = true)] + public string Name { get; set; } + + /// + /// Gets or Sets RouteEpoch + /// + [DataMember(Name = "route_epoch", IsRequired = true, EmitDefaultValue = true)] + public long RouteEpoch { get; set; } + + /// + /// Gets or Sets Subnets + /// + [DataMember(Name = "subnets", IsRequired = true, EmitDefaultValue = true)] + public List Subnets { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class AgentInfo {\n"); + sb.Append(" AgentId: ").Append(AgentId).Append("\n"); + sb.Append(" CertFingerprint: ").Append(CertFingerprint).Append("\n"); + sb.Append(" Domains: ").Append(Domains).Append("\n"); + sb.Append(" IsOnline: ").Append(IsOnline).Append("\n"); + sb.Append(" LastSeenMs: ").Append(LastSeenMs).Append("\n"); + sb.Append(" Name: ").Append(Name).Append("\n"); + sb.Append(" RouteEpoch: ").Append(RouteEpoch).Append("\n"); + sb.Append(" Subnets: ").Append(Subnets).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + // LastSeenMs (long) minimum + if (this.LastSeenMs < (long)0) + { + yield return new ValidationResult("Invalid value for LastSeenMs, must be a value greater than or equal to 0.", new [] { "LastSeenMs" }); + } + + // RouteEpoch (long) minimum + if (this.RouteEpoch < (long)0) + { + yield return new ValidationResult("Invalid value for RouteEpoch, must be a value greater than or equal to 0.", new [] { "RouteEpoch" }); + } + + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EnrollRequest.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EnrollRequest.cs new file mode 100644 index 000000000..a8a4b507d --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EnrollRequest.cs @@ -0,0 +1,115 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// EnrollRequest + /// + [DataContract(Name = "EnrollRequest")] + public partial class EnrollRequest : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected EnrollRequest() { } + /// + /// Initializes a new instance of the class. + /// + /// Optional hostname of the agent machine (added as DNS SAN in the issued certificate).. + /// Agent-generated UUID (the agent owns its identity). (required). + /// PEM-encoded Certificate Signing Request from the agent. (required). + public EnrollRequest(string agentHostname = default(string), Guid agentId = default(Guid), string csrPem = default(string)) + { + this.AgentId = agentId; + // to ensure "csrPem" is required (not null) + if (csrPem == null) + { + throw new ArgumentNullException("csrPem is a required property for EnrollRequest and cannot be null"); + } + this.CsrPem = csrPem; + this.AgentHostname = agentHostname; + } + + /// + /// Optional hostname of the agent machine (added as DNS SAN in the issued certificate). + /// + /// Optional hostname of the agent machine (added as DNS SAN in the issued certificate). + [DataMember(Name = "agent_hostname", EmitDefaultValue = true)] + public string AgentHostname { get; set; } + + /// + /// Agent-generated UUID (the agent owns its identity). + /// + /// Agent-generated UUID (the agent owns its identity). + [DataMember(Name = "agent_id", IsRequired = true, EmitDefaultValue = true)] + public Guid AgentId { get; set; } + + /// + /// PEM-encoded Certificate Signing Request from the agent. + /// + /// PEM-encoded Certificate Signing Request from the agent. + [DataMember(Name = "csr_pem", IsRequired = true, EmitDefaultValue = true)] + public string CsrPem { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class EnrollRequest {\n"); + sb.Append(" AgentHostname: ").Append(AgentHostname).Append("\n"); + sb.Append(" AgentId: ").Append(AgentId).Append("\n"); + sb.Append(" CsrPem: ").Append(CsrPem).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EnrollResponse.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EnrollResponse.cs new file mode 100644 index 000000000..00fd3ef98 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/EnrollResponse.cs @@ -0,0 +1,150 @@ +/* + * devolutions-gateway + * + * Protocol-aware fine-grained relay server + * + * The version of the OpenAPI document: 2026.2.4 + * Contact: infos@devolutions.net + * Generated by: https://github.com/openapitools/openapi-generator.git + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.IO; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using System.ComponentModel.DataAnnotations; +using FileParameter = Devolutions.Gateway.Client.Client.FileParameter; +using OpenAPIDateConverter = Devolutions.Gateway.Client.Client.OpenAPIDateConverter; + +namespace Devolutions.Gateway.Client.Model +{ + /// + /// EnrollResponse + /// + [DataContract(Name = "EnrollResponse")] + public partial class EnrollResponse : IValidatableObject + { + /// + /// Initializes a new instance of the class. + /// + [JsonConstructorAttribute] + protected EnrollResponse() { } + /// + /// Initializes a new instance of the class. + /// + /// Assigned agent ID. (required). + /// PEM-encoded client certificate (signed by the gateway CA). (required). + /// PEM-encoded gateway CA certificate (for server verification). (required). + /// QUIC endpoint to connect to (`host:port`). (required). + /// SHA-256 hash of the server certificate's SPKI (hex-encoded). Used by the agent to pin the server's public key. (required). + public EnrollResponse(Guid agentId = default(Guid), string clientCertPem = default(string), string gatewayCaCertPem = default(string), string quicEndpoint = default(string), string serverSpkiSha256 = default(string)) + { + this.AgentId = agentId; + // to ensure "clientCertPem" is required (not null) + if (clientCertPem == null) + { + throw new ArgumentNullException("clientCertPem is a required property for EnrollResponse and cannot be null"); + } + this.ClientCertPem = clientCertPem; + // to ensure "gatewayCaCertPem" is required (not null) + if (gatewayCaCertPem == null) + { + throw new ArgumentNullException("gatewayCaCertPem is a required property for EnrollResponse and cannot be null"); + } + this.GatewayCaCertPem = gatewayCaCertPem; + // to ensure "quicEndpoint" is required (not null) + if (quicEndpoint == null) + { + throw new ArgumentNullException("quicEndpoint is a required property for EnrollResponse and cannot be null"); + } + this.QuicEndpoint = quicEndpoint; + // to ensure "serverSpkiSha256" is required (not null) + if (serverSpkiSha256 == null) + { + throw new ArgumentNullException("serverSpkiSha256 is a required property for EnrollResponse and cannot be null"); + } + this.ServerSpkiSha256 = serverSpkiSha256; + } + + /// + /// Assigned agent ID. + /// + /// Assigned agent ID. + [DataMember(Name = "agent_id", IsRequired = true, EmitDefaultValue = true)] + public Guid AgentId { get; set; } + + /// + /// PEM-encoded client certificate (signed by the gateway CA). + /// + /// PEM-encoded client certificate (signed by the gateway CA). + [DataMember(Name = "client_cert_pem", IsRequired = true, EmitDefaultValue = true)] + public string ClientCertPem { get; set; } + + /// + /// PEM-encoded gateway CA certificate (for server verification). + /// + /// PEM-encoded gateway CA certificate (for server verification). + [DataMember(Name = "gateway_ca_cert_pem", IsRequired = true, EmitDefaultValue = true)] + public string GatewayCaCertPem { get; set; } + + /// + /// QUIC endpoint to connect to (`host:port`). + /// + /// QUIC endpoint to connect to (`host:port`). + [DataMember(Name = "quic_endpoint", IsRequired = true, EmitDefaultValue = true)] + public string QuicEndpoint { get; set; } + + /// + /// SHA-256 hash of the server certificate's SPKI (hex-encoded). Used by the agent to pin the server's public key. + /// + /// SHA-256 hash of the server certificate's SPKI (hex-encoded). Used by the agent to pin the server's public key. + [DataMember(Name = "server_spki_sha256", IsRequired = true, EmitDefaultValue = true)] + public string ServerSpkiSha256 { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class EnrollResponse {\n"); + sb.Append(" AgentId: ").Append(AgentId).Append("\n"); + sb.Append(" ClientCertPem: ").Append(ClientCertPem).Append("\n"); + sb.Append(" GatewayCaCertPem: ").Append(GatewayCaCertPem).Append("\n"); + sb.Append(" QuicEndpoint: ").Append(QuicEndpoint).Append("\n"); + sb.Append(" ServerSpkiSha256: ").Append(ServerSpkiSha256).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); + } + + /// + /// To validate all properties of the instance + /// + /// Validation context + /// Validation Result + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) + { + yield break; + } + } + +} diff --git a/devolutions-gateway/openapi/gateway-api.yaml b/devolutions-gateway/openapi/gateway-api.yaml index 851fdd6af..92280ed88 100644 --- a/devolutions-gateway/openapi/gateway-api.yaml +++ b/devolutions-gateway/openapi/gateway-api.yaml @@ -958,6 +958,125 @@ paths: security: - scope_token: - gateway.traffic.claim + /jet/tunnel/agents: + get: + tags: + - Agent + summary: List connected agents and their status. + operationId: ListAgents + responses: + '200': + description: Connected agents + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AgentInfo' + '401': + description: Invalid or missing authorization token + '403': + description: Insufficient permissions + '500': + description: Unexpected server error + security: + - scope_token: + - gateway.agent.read + /jet/tunnel/agents/{agent_id}: + get: + tags: + - Agent + summary: Get a single agent by ID. + operationId: GetAgent + parameters: + - name: agent_id + in: path + description: Agent ID + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Agent status + content: + application/json: + schema: + $ref: '#/components/schemas/AgentInfo' + '401': + description: Invalid or missing authorization token + '403': + description: Insufficient permissions + '404': + description: Agent not found + '500': + description: Unexpected server error + security: + - scope_token: + - gateway.agent.read + delete: + tags: + - Agent + summary: Delete (unregister) an agent by ID. + operationId: DeleteAgent + parameters: + - name: agent_id + in: path + description: Agent ID + required: true + schema: + type: string + format: uuid + responses: + '204': + description: Agent deleted + '401': + description: Invalid or missing authorization token + '403': + description: Insufficient permissions + '404': + description: Agent not found + '500': + description: Unexpected server error + security: + - scope_token: + - gateway.agent.delete + /jet/tunnel/enroll: + post: + tags: + - Agent + summary: Enroll a new agent. + description: |- + Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key + (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). + + The agent generates its own key pair and sends a CSR. The gateway signs it + and returns the certificate. The private key never leaves the agent. + operationId: EnrollAgent + requestBody: + description: Agent identity and certificate signing request + content: + application/json: + schema: + $ref: '#/components/schemas/EnrollRequest' + required: true + responses: + '200': + description: Agent enrolled + content: + application/json: + schema: + $ref: '#/components/schemas/EnrollResponse' + '400': + description: Invalid agent name, request body, or certificate signing request + '401': + description: Invalid or missing enrollment token + '409': + description: Agent ID already registered + '500': + description: Unexpected server error + security: + - enrollment_token: [] /jet/update: get: tags: @@ -1216,6 +1335,53 @@ components: enum: - IPv4 - IPv6 + AgentDomainAdvertisement: + type: object + required: + - domain + - auto_detected + properties: + auto_detected: + type: boolean + domain: + type: string + AgentInfo: + type: object + required: + - agent_id + - name + - cert_fingerprint + - is_online + - last_seen_ms + - subnets + - domains + - route_epoch + properties: + agent_id: + type: string + format: uuid + cert_fingerprint: + type: string + domains: + type: array + items: + $ref: '#/components/schemas/AgentDomainAdvertisement' + is_online: + type: boolean + last_seen_ms: + type: integer + format: int64 + minimum: 0 + name: + type: string + route_epoch: + type: integer + format: int64 + minimum: 0 + subnets: + type: array + items: + type: string AppCredential: type: object required: @@ -1357,6 +1523,50 @@ components: type: integer description: Number of recordings not found minimum: 0 + EnrollRequest: + type: object + required: + - agent_id + - csr_pem + properties: + agent_hostname: + type: string + description: Optional hostname of the agent machine (added as DNS SAN in the issued certificate). + nullable: true + agent_id: + type: string + format: uuid + description: Agent-generated UUID (the agent owns its identity). + csr_pem: + type: string + description: PEM-encoded Certificate Signing Request from the agent. + EnrollResponse: + type: object + required: + - agent_id + - client_cert_pem + - gateway_ca_cert_pem + - quic_endpoint + - server_spki_sha256 + properties: + agent_id: + type: string + format: uuid + description: Assigned agent ID. + client_cert_pem: + type: string + description: PEM-encoded client certificate (signed by the gateway CA). + gateway_ca_cert_pem: + type: string + description: PEM-encoded gateway CA certificate (for server verification). + quic_endpoint: + type: string + description: QUIC endpoint to connect to (`host:port`). + server_spki_sha256: + type: string + description: |- + SHA-256 hash of the server certificate's SPKI (hex-encoded). + Used by the agent to pin the server's public key. EventOutcomeResponse: type: string enum: @@ -2251,6 +2461,11 @@ components: type: object description: Response returned by the update endpoint. securitySchemes: + enrollment_token: + type: http + scheme: bearer + bearerFormat: JWT + description: Single-use token authorizing Agent Tunnel enrollment jrec_token: type: http scheme: bearer diff --git a/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES b/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES index 8a388ef76..42919a637 100644 --- a/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES +++ b/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES @@ -1,6 +1,7 @@ .gitignore README.md api.module.ts +api/agent.service.ts api/api.ts api/config.service.ts api/diagnostics.service.ts @@ -22,6 +23,8 @@ model/accessScope.ts model/ackRequest.ts model/ackResponse.ts model/addressFamily.ts +model/agentDomainAdvertisement.ts +model/agentInfo.ts model/appCredential.ts model/appCredentialKind.ts model/appTokenContentType.ts @@ -33,6 +36,8 @@ model/configPatch.ts model/connectionMode.ts model/dataEncoding.ts model/deleteManyResult.ts +model/enrollRequest.ts +model/enrollResponse.ts model/eventOutcomeResponse.ts model/getUpdateProductsResponse.ts model/getUpdateScheduleResponse.ts diff --git a/devolutions-gateway/openapi/ts-angular-client/api/agent.service.ts b/devolutions-gateway/openapi/ts-angular-client/api/agent.service.ts new file mode 100644 index 000000000..760d4f0de --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/api/agent.service.ts @@ -0,0 +1,387 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { AgentInfo } from '../model/agentInfo'; +// @ts-ignore +import { EnrollRequest } from '../model/enrollRequest'; +// @ts-ignore +import { EnrollResponse } from '../model/enrollResponse'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; + + + +@Injectable({ + providedIn: 'root' +}) +export class AgentService { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * Delete (unregister) an agent by ID. + * @param agentId Agent ID + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteAgent(agentId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; + public deleteAgent(agentId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteAgent(agentId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteAgent(agentId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { + if (agentId === null || agentId === undefined) { + throw new Error('Required parameter agentId was null or undefined when calling deleteAgent.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (scope_token) required + localVarCredential = this.configuration.lookupCredential('scope_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/tunnel/agents/${this.configuration.encodeParam({name: "agentId", value: agentId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Enroll a new agent. + * Requires a Bearer token: an `ENROLLMENT` JWT signed by the configured provisioner key (e.g. DVLS, Hub, PAM service, or any other compatible provisioner). The agent generates its own key pair and sends a CSR. The gateway signs it and returns the certificate. The private key never leaves the agent. + * @param enrollRequest Agent identity and certificate signing request + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public enrollAgent(enrollRequest: EnrollRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public enrollAgent(enrollRequest: EnrollRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public enrollAgent(enrollRequest: EnrollRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public enrollAgent(enrollRequest: EnrollRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (enrollRequest === null || enrollRequest === undefined) { + throw new Error('Required parameter enrollRequest was null or undefined when calling enrollAgent.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (enrollment_token) required + localVarCredential = this.configuration.lookupCredential('enrollment_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/tunnel/enroll`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: enrollRequest, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Get a single agent by ID. + * @param agentId Agent ID + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getAgent(agentId: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getAgent(agentId: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAgent(agentId: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getAgent(agentId: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (agentId === null || agentId === undefined) { + throw new Error('Required parameter agentId was null or undefined when calling getAgent.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (scope_token) required + localVarCredential = this.configuration.lookupCredential('scope_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/tunnel/agents/${this.configuration.encodeParam({name: "agentId", value: agentId, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: "uuid"})}`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * List connected agents and their status. + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public listAgents(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public listAgents(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public listAgents(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public listAgents(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (scope_token) required + localVarCredential = this.configuration.lookupCredential('scope_token'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/jet/tunnel/agents`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/devolutions-gateway/openapi/ts-angular-client/api/api.ts b/devolutions-gateway/openapi/ts-angular-client/api/api.ts index 8b746c95e..a7eb029c9 100644 --- a/devolutions-gateway/openapi/ts-angular-client/api/api.ts +++ b/devolutions-gateway/openapi/ts-angular-client/api/api.ts @@ -1,3 +1,5 @@ +export * from './agent.service'; +import { AgentService } from './agent.service'; export * from './config.service'; import { ConfigService } from './config.service'; export * from './diagnostics.service'; @@ -24,4 +26,4 @@ export * from './update.service'; import { UpdateService } from './update.service'; export * from './webApp.service'; import { WebAppService } from './webApp.service'; -export const APIS = [ConfigService, DiagnosticsService, HealthService, HeartbeatService, JrecService, JrlService, NetService, NetworkMonitoringService, PreflightService, SessionsService, TrafficService, UpdateService, WebAppService]; +export const APIS = [AgentService, ConfigService, DiagnosticsService, HealthService, HeartbeatService, JrecService, JrlService, NetService, NetworkMonitoringService, PreflightService, SessionsService, TrafficService, UpdateService, WebAppService]; diff --git a/devolutions-gateway/openapi/ts-angular-client/configuration.ts b/devolutions-gateway/openapi/ts-angular-client/configuration.ts index 174134f4b..f11d4c083 100644 --- a/devolutions-gateway/openapi/ts-angular-client/configuration.ts +++ b/devolutions-gateway/openapi/ts-angular-client/configuration.ts @@ -87,6 +87,15 @@ export class Configuration { this.credentials = {}; } + // init default enrollment_token credential + if (!this.credentials['enrollment_token']) { + this.credentials['enrollment_token'] = () => { + return typeof this.accessToken === 'function' + ? this.accessToken() + : this.accessToken; + }; + } + // init default jrec_token credential if (!this.credentials['jrec_token']) { this.credentials['jrec_token'] = () => { diff --git a/devolutions-gateway/openapi/ts-angular-client/model/agentDomainAdvertisement.ts b/devolutions-gateway/openapi/ts-angular-client/model/agentDomainAdvertisement.ts new file mode 100644 index 000000000..ca24a7fbd --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/agentDomainAdvertisement.ts @@ -0,0 +1,16 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface AgentDomainAdvertisement { + auto_detected: boolean; + domain: string; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/agentInfo.ts b/devolutions-gateway/openapi/ts-angular-client/model/agentInfo.ts new file mode 100644 index 000000000..e261788c4 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/agentInfo.ts @@ -0,0 +1,23 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { AgentDomainAdvertisement } from './agentDomainAdvertisement'; + + +export interface AgentInfo { + agent_id: string; + cert_fingerprint: string; + domains: Array; + is_online: boolean; + last_seen_ms: number; + name: string; + route_epoch: number; + subnets: Array; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/enrollRequest.ts b/devolutions-gateway/openapi/ts-angular-client/model/enrollRequest.ts new file mode 100644 index 000000000..f72bcf658 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/enrollRequest.ts @@ -0,0 +1,26 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface EnrollRequest { + /** + * Optional hostname of the agent machine (added as DNS SAN in the issued certificate). + */ + agent_hostname?: string | null; + /** + * Agent-generated UUID (the agent owns its identity). + */ + agent_id: string; + /** + * PEM-encoded Certificate Signing Request from the agent. + */ + csr_pem: string; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/enrollResponse.ts b/devolutions-gateway/openapi/ts-angular-client/model/enrollResponse.ts new file mode 100644 index 000000000..81ee97ee5 --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/enrollResponse.ts @@ -0,0 +1,34 @@ +/** + * devolutions-gateway + * + * Contact: infos@devolutions.net + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface EnrollResponse { + /** + * Assigned agent ID. + */ + agent_id: string; + /** + * PEM-encoded client certificate (signed by the gateway CA). + */ + client_cert_pem: string; + /** + * PEM-encoded gateway CA certificate (for server verification). + */ + gateway_ca_cert_pem: string; + /** + * QUIC endpoint to connect to (`host:port`). + */ + quic_endpoint: string; + /** + * SHA-256 hash of the server certificate\'s SPKI (hex-encoded). Used by the agent to pin the server\'s public key. + */ + server_spki_sha256: string; +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/models.ts b/devolutions-gateway/openapi/ts-angular-client/model/models.ts index f7c62b504..4675ea6a7 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/models.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/models.ts @@ -2,6 +2,8 @@ export * from './accessScope'; export * from './ackRequest'; export * from './ackResponse'; export * from './addressFamily'; +export * from './agentDomainAdvertisement'; +export * from './agentInfo'; export * from './appCredential'; export * from './appCredentialKind'; export * from './appTokenContentType'; @@ -13,6 +15,8 @@ export * from './configPatch'; export * from './connectionMode'; export * from './dataEncoding'; export * from './deleteManyResult'; +export * from './enrollRequest'; +export * from './enrollResponse'; export * from './eventOutcomeResponse'; export * from './getUpdateProductsResponse'; export * from './getUpdateScheduleResponse'; diff --git a/devolutions-gateway/src/api/tunnel.rs b/devolutions-gateway/src/api/tunnel.rs index f8534bd9f..4326071cc 100644 --- a/devolutions-gateway/src/api/tunnel.rs +++ b/devolutions-gateway/src/api/tunnel.rs @@ -8,6 +8,7 @@ use crate::extract::{AgentManagementDeleteAccess, AgentManagementReadAccess}; use crate::http::HttpError; #[derive(Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct EnrollRequest { /// Agent-generated UUID (the agent owns its identity). pub agent_id: Uuid, @@ -19,6 +20,7 @@ pub struct EnrollRequest { } #[derive(Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct EnrollResponse { /// Assigned agent ID. pub agent_id: Uuid, @@ -33,6 +35,48 @@ pub struct EnrollResponse { pub server_spki_sha256: String, } +#[derive(Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct AgentDomainAdvertisement { + pub domain: String, + pub auto_detected: bool, +} + +#[derive(Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct AgentInfo { + pub agent_id: Uuid, + pub name: String, + pub cert_fingerprint: String, + pub is_online: bool, + pub last_seen_ms: u64, + pub subnets: Vec, + pub domains: Vec, + pub route_epoch: u64, +} + +impl From for AgentInfo { + fn from(info: agent_tunnel::registry::AgentInfo) -> Self { + Self { + agent_id: info.agent_id, + name: info.name, + cert_fingerprint: info.cert_fingerprint, + is_online: info.is_online, + last_seen_ms: info.last_seen_ms, + subnets: info.subnets, + domains: info + .domains + .into_iter() + .map(|domain| AgentDomainAdvertisement { + domain: domain.domain.to_string(), + auto_detected: domain.auto_detected, + }) + .collect(), + route_epoch: info.route_epoch, + } + } +} + pub fn make_router(state: DgwState) -> Router { Router::new() .route("/enroll", axum::routing::post(enroll_agent)) @@ -48,7 +92,22 @@ pub fn make_router(state: DgwState) -> Router { /// /// The agent generates its own key pair and sends a CSR. The gateway signs it /// and returns the certificate. The private key never leaves the agent. -async fn enroll_agent( +#[cfg_attr(feature = "openapi", utoipa::path( + post, + operation_id = "EnrollAgent", + tag = "Agent", + path = "/jet/tunnel/enroll", + request_body(content = EnrollRequest, description = "Agent identity and certificate signing request", content_type = "application/json"), + responses( + (status = 200, description = "Agent enrolled", body = EnrollResponse), + (status = 400, description = "Invalid agent name, request body, or certificate signing request"), + (status = 401, description = "Invalid or missing enrollment token"), + (status = 409, description = "Agent ID already registered"), + (status = 500, description = "Unexpected server error"), + ), + security(("enrollment_token" = [])), +))] +pub(crate) async fn enroll_agent( crate::extract::EnrollmentToken(token_claims): crate::extract::EnrollmentToken, State(DgwState { conf_handle, @@ -109,29 +168,65 @@ async fn enroll_agent( } /// List connected agents and their status. -async fn list_agents( +#[cfg_attr(feature = "openapi", utoipa::path( + get, + operation_id = "ListAgents", + tag = "Agent", + path = "/jet/tunnel/agents", + responses( + (status = 200, description = "Connected agents", body = [AgentInfo]), + (status = 401, description = "Invalid or missing authorization token"), + (status = 403, description = "Insufficient permissions"), + (status = 500, description = "Unexpected server error"), + ), + security(("scope_token" = ["gateway.agent.read"])), +))] +pub(crate) async fn list_agents( State(DgwState { agent_tunnel_handle, .. }): State, _access: AgentManagementReadAccess, -) -> Result>, HttpError> { +) -> Result>, HttpError> { let handle = agent_tunnel_handle .as_ref() .ok_or_else(|| HttpError::not_found().msg("agent tunnel not configured"))?; - let agents = handle.registry().agent_infos().await; + let agents = handle + .registry() + .agent_infos() + .await + .into_iter() + .map(AgentInfo::from) + .collect(); Ok(Json(agents)) } /// Get a single agent by ID. -async fn get_agent( +#[cfg_attr(feature = "openapi", utoipa::path( + get, + operation_id = "GetAgent", + tag = "Agent", + path = "/jet/tunnel/agents/{agent_id}", + params( + ("agent_id" = Uuid, Path, description = "Agent ID") + ), + responses( + (status = 200, description = "Agent status", body = AgentInfo), + (status = 401, description = "Invalid or missing authorization token"), + (status = 403, description = "Insufficient permissions"), + (status = 404, description = "Agent not found"), + (status = 500, description = "Unexpected server error"), + ), + security(("scope_token" = ["gateway.agent.read"])), +))] +pub(crate) async fn get_agent( _access: AgentManagementReadAccess, State(DgwState { agent_tunnel_handle, .. }): State, Path(agent_id): Path, -) -> Result, HttpError> { +) -> Result, HttpError> { let handle = agent_tunnel_handle .as_ref() .ok_or_else(|| HttpError::not_found().msg("agent tunnel not configured"))?; @@ -142,11 +237,28 @@ async fn get_agent( .await .ok_or_else(|| HttpError::not_found().msg("agent not found"))?; - Ok(Json(info)) + Ok(Json(info.into())) } /// Delete (unregister) an agent by ID. -async fn delete_agent( +#[cfg_attr(feature = "openapi", utoipa::path( + delete, + operation_id = "DeleteAgent", + tag = "Agent", + path = "/jet/tunnel/agents/{agent_id}", + params( + ("agent_id" = Uuid, Path, description = "Agent ID") + ), + responses( + (status = 204, description = "Agent deleted"), + (status = 401, description = "Invalid or missing authorization token"), + (status = 403, description = "Insufficient permissions"), + (status = 404, description = "Agent not found"), + (status = 500, description = "Unexpected server error"), + ), + security(("scope_token" = ["gateway.agent.delete"])), +))] +pub(crate) async fn delete_agent( _access: AgentManagementDeleteAccess, State(DgwState { agent_tunnel_handle, .. diff --git a/devolutions-gateway/src/openapi.rs b/devolutions-gateway/src/openapi.rs index 73b22bf60..0ea57027b 100644 --- a/devolutions-gateway/src/openapi.rs +++ b/devolutions-gateway/src/openapi.rs @@ -38,6 +38,10 @@ use crate::config::dto::{DataEncoding, PubKeyFormat, Subscriber}; crate::api::monitoring::handle_drain_log, crate::api::traffic::post_traffic_claim, crate::api::traffic::post_traffic_ack, + crate::api::tunnel::enroll_agent, + crate::api::tunnel::list_agents, + crate::api::tunnel::get_agent, + crate::api::tunnel::delete_agent, ), components(schemas( crate::api::health::Identity, @@ -99,6 +103,10 @@ use crate::config::dto::{DataEncoding, PubKeyFormat, Subscriber}; crate::api::traffic::TrafficEventResponse, crate::api::traffic::EventOutcomeResponse, crate::api::traffic::TransportProtocolResponse, + crate::api::tunnel::EnrollRequest, + crate::api::tunnel::EnrollResponse, + crate::api::tunnel::AgentDomainAdvertisement, + crate::api::tunnel::AgentInfo, )), modifiers(&SecurityAddon), )] @@ -217,6 +225,17 @@ impl Modify for SecurityAddon { .build(), ), ); + + components.add_security_scheme( + "enrollment_token", + SecurityScheme::Http( + HttpBuilder::new() + .scheme(HttpAuthScheme::Bearer) + .bearer_format("JWT") + .description(Some("Single-use token authorizing Agent Tunnel enrollment".to_owned())) + .build(), + ), + ); } }