From 0cfcd58ae18590bf2abb6f05ba9b6a36ade52c74 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Fri, 28 Aug 2026 16:10:42 -0400 Subject: [PATCH 1/8] feat(dgw): clarify agent tunnel status Expose online, unresponsive, and offline Agent states through the management endpoints. Keep routing information available when runtime state exists, and stop publishing certificate fingerprints and route synchronization epochs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- devolutions-gateway/src/api/tunnel.rs | 35 ++++++++++++++++++++------- testsuite/tests/cli/agent/tunnel.rs | 11 ++++----- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/devolutions-gateway/src/api/tunnel.rs b/devolutions-gateway/src/api/tunnel.rs index 6c3af7254..3c222e15e 100644 --- a/devolutions-gateway/src/api/tunnel.rs +++ b/devolutions-gateway/src/api/tunnel.rs @@ -37,20 +37,37 @@ pub struct EnrollResponse { #[derive(Serialize)] pub struct AgentDomainAdvertisement { + /// Domain route advertised by the Agent. pub domain: String, + /// Whether the Agent discovered the domain automatically. pub auto_detected: bool, } +#[derive(Clone, Copy, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AgentStatus { + /// No tunnel connection exists for the Agent. + Offline, + /// The Agent has a tunnel connection and a recent heartbeat. + Online, + /// The Agent has a tunnel connection, but its heartbeat has expired. + Unresponsive, +} + #[derive(Serialize)] pub struct AgentInfo { + /// Stable Agent identity. pub agent_id: Uuid, + /// Unique management name assigned during enrollment. pub name: String, - pub cert_fingerprint: Option, - pub is_online: bool, + /// Current tunnel connection status. + pub status: AgentStatus, + /// Last heartbeat timestamp in milliseconds since the Unix epoch. pub last_seen_ms: Option, + /// Subnet routes currently advertised by the Agent. pub subnets: Option>, + /// Domain routes currently advertised by the Agent. pub domains: Option>, - pub route_epoch: Option, } pub fn make_router(state: DgwState) -> Router { @@ -194,20 +211,21 @@ fn agent_info( return AgentInfo { agent_id: accepted.agent_id, name: accepted.name, - cert_fingerprint: None, - is_online: false, + status: AgentStatus::Offline, last_seen_ms: None, subnets: None, domains: None, - route_epoch: None, }; }; AgentInfo { agent_id: accepted.agent_id, name: accepted.name, - cert_fingerprint: Some(runtime.cert_fingerprint), - is_online: runtime.is_online, + status: if runtime.is_online { + AgentStatus::Online + } else { + AgentStatus::Unresponsive + }, last_seen_ms: Some(runtime.last_seen_ms), subnets: Some(runtime.subnets), domains: Some( @@ -220,7 +238,6 @@ fn agent_info( }) .collect(), ), - route_epoch: Some(runtime.route_epoch), } } diff --git a/testsuite/tests/cli/agent/tunnel.rs b/testsuite/tests/cli/agent/tunnel.rs index 55a5e1dae..d6460ae57 100644 --- a/testsuite/tests/cli/agent/tunnel.rs +++ b/testsuite/tests/cli/agent/tunnel.rs @@ -473,8 +473,7 @@ async fn wait_for_registered_agent( }) }); - if agent.get("is_online").and_then(serde_json::Value::as_bool) == Some(true) - && agent.get("route_epoch").and_then(serde_json::Value::as_u64) == Some(1) + if agent.get("status").and_then(serde_json::Value::as_str) == Some("online") && subnets == expected_subnets && domains == expected_domains && domains_are_explicit @@ -680,14 +679,14 @@ async fn enrolled_agent_forwards_domain_only_route_and_reconnects() { Some("smoke-agent") ); assert_eq!( - offline.get("is_online").and_then(serde_json::Value::as_bool), - Some(false) + offline.get("status").and_then(serde_json::Value::as_str), + Some("offline") ); - assert!(offline.get("cert_fingerprint").is_some_and(serde_json::Value::is_null)); assert!(offline.get("last_seen_ms").is_some_and(serde_json::Value::is_null)); assert!(offline.get("subnets").is_some_and(serde_json::Value::is_null)); assert!(offline.get("domains").is_some_and(serde_json::Value::is_null)); - assert!(offline.get("route_epoch").is_some_and(serde_json::Value::is_null)); + assert!(offline.get("cert_fingerprint").is_none()); + assert!(offline.get("route_epoch").is_none()); let mut agent = agent_tokio_cmd() .env("DAGENT_CONFIG_PATH", agent_config.path()) .arg("run") From 068c0bc871e0a3bd432ffe11b5067c3b8b63cc8d Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Fri, 28 Aug 2026 16:33:25 -0400 Subject: [PATCH 2/8] test(dgw): cover unresponsive agent status Verify that a connected Agent with an expired heartbeat is reported as unresponsive while retaining its last runtime snapshot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- devolutions-gateway/src/api/tunnel.rs | 37 ++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/devolutions-gateway/src/api/tunnel.rs b/devolutions-gateway/src/api/tunnel.rs index 3c222e15e..ffd7270d6 100644 --- a/devolutions-gateway/src/api/tunnel.rs +++ b/devolutions-gateway/src/api/tunnel.rs @@ -43,7 +43,7 @@ pub struct AgentDomainAdvertisement { pub auto_detected: bool, } -#[derive(Clone, Copy, Serialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum AgentStatus { /// No tunnel connection exists for the Agent. @@ -309,3 +309,38 @@ async fn delete_agent( Ok(axum::http::StatusCode::NO_CONTENT) } + +#[cfg(test)] +mod tests { + use super::*; + + fn accepted_agent(agent_id: Uuid) -> agent_tunnel::authorization::AcceptedAgent { + agent_tunnel::authorization::AcceptedAgent { + agent_id, + name: String::from("montreal-office"), + client_spki_sha256: [0x11; 32], + } + } + + #[test] + fn connected_agent_without_recent_heartbeat_is_unresponsive() { + let agent_id = Uuid::new_v4(); + let runtime = agent_tunnel::registry::AgentInfo { + agent_id, + name: String::from("montreal-office"), + cert_fingerprint: String::from("fingerprint"), + is_online: false, + last_seen_ms: 1234, + subnets: Vec::new(), + domains: Vec::new(), + route_epoch: 1, + }; + + let info = agent_info(accepted_agent(agent_id), Some(runtime)); + + assert_eq!(info.status, AgentStatus::Unresponsive); + assert_eq!(info.last_seen_ms, Some(1234)); + assert_eq!(info.subnets, Some(Vec::new())); + assert!(info.domains.is_some_and(|domains| domains.is_empty())); + } +} From 147cbf986cd8b85bf0c33bea886ed32b1cf9324a Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 24 Aug 2026 15:08:31 -0400 Subject: [PATCH 3/8] 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 62a884911..d4d42bf50 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 72de8ee99..646b63c68 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 d6460ae57..c36e22ca9 100644 --- a/testsuite/tests/cli/agent/tunnel.rs +++ b/testsuite/tests/cli/agent/tunnel.rs @@ -640,7 +640,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"); @@ -775,7 +774,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 4601d3a368603539f2a117c1591eccc5b544bd97 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 24 Aug 2026 18:02:37 -0400 Subject: [PATCH 4/8] 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 | 82 ++++++++----------- 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, 52 insertions(+), 114 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 d4d42bf50..e0970e991 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 646b63c68..e371f477f 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 a67718ead..d0a4dc71f 100644 --- a/devolutions-gateway/src/service.rs +++ b/devolutions-gateway/src/service.rs @@ -275,55 +275,43 @@ 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 authorization_database = data_dir.join("agent_tunnel.db"); - let ca_manager = if authorization_database.exists() { - agent_tunnel::cert::CaManager::load(&data_dir) - } else { - agent_tunnel::cert::CaManager::load_or_generate(&data_dir) - } - .context("failed to initialize agent tunnel CA")?; - let ca_spki_sha256 = ca_manager - .ca_spki_sha256() - .context("failed to identify agent tunnel CA")?; - let authorization_store = - agent_tunnel_libsql::LibSqlAgentAuthorizationStore::open(authorization_database.as_str(), ca_spki_sha256) - .await - .context("failed to initialize Agent authorization database")?; - let authorization_store: agent_tunnel::authorization::DynAgentAuthorizationStore = - Arc::new(authorization_store); - - // 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, - authorization_store, - ) - .await - .context("failed to bind agent tunnel listener")?; + let data_dir = config::get_data_dir(); + let hostname = &conf.hostname; - tasks.register(listener); + let authorization_database = data_dir.join("agent_tunnel.db"); + let ca_manager = if authorization_database.exists() { + agent_tunnel::cert::CaManager::load(&data_dir) + } else { + agent_tunnel::cert::CaManager::load_or_generate(&data_dir) + } + .context("failed to initialize agent tunnel CA")?; + let ca_spki_sha256 = ca_manager + .ca_spki_sha256() + .context("failed to identify agent tunnel CA")?; + let authorization_store = + agent_tunnel_libsql::LibSqlAgentAuthorizationStore::open(authorization_database.as_str(), ca_spki_sha256) + .await + .context("failed to initialize Agent authorization database")?; + let authorization_store: agent_tunnel::authorization::DynAgentAuthorizationStore = Arc::new(authorization_store); + + // 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 (agent_tunnel_listener, agent_tunnel_handle) = + agent_tunnel::AgentTunnelListener::bind(listen_addr, Arc::clone(&ca_manager), hostname, authorization_store) + .await + .context("failed to bind agent tunnel listener")?; - info!( - port = conf.agent_tunnel.listen_port, - "Agent tunnel QUIC listener started", - ); + tasks.register(agent_tunnel_listener); - Some(Arc::new(handle)) - } else { - None - }; + info!( + port = conf.agent_tunnel.listen_port, + "Agent tunnel QUIC listener started", + ); let state = DgwState { conf_handle: conf_handle.clone(), @@ -338,7 +326,7 @@ async fn spawn_tasks(conf_handle: ConfHandle) -> anyhow::Result { synthetic_kdc_registry: synthetic_kdc_registry.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 c36e22ca9..36415344a 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; @@ -639,7 +639,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"); @@ -773,7 +772,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 cf7a4b913db4124eac92e2988857d3036602aff0 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 24 Aug 2026 18:21:22 -0400 Subject: [PATCH 5/8] 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 e0970e991..43dc46326 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 e371f477f..0f07741dd 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 d0a4dc71f..a0a62816b 100644 --- a/devolutions-gateway/src/service.rs +++ b/devolutions-gateway/src/service.rs @@ -299,7 +299,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, authorization_store) @@ -309,7 +310,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 5f8fd0212d7fb4299ac3abca3f5eec154d5bae53 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Mon, 24 Aug 2026 19:48:56 -0400 Subject: [PATCH 6/8] 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 + .../dotnet-client/docs/PreflightOperation.md | 3 +- .../Api/AgentApi.cs | 893 ++++++++++++++++++ .../Model/AgentDomainAdvertisement.cs | 103 ++ .../Model/AgentInfo.cs | 169 ++++ .../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 | 74 +- devolutions-gateway/src/openapi.rs | 19 + 26 files changed, 3300 insertions(+), 11 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 b40865063..3f65dcb5f 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 an accepted 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 accepted agents and their current 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 +| Accepted agents and their current status +| 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..8712100cc 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 an accepted 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 an accepted 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 accepted agents and their current 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..fb9bbb515 --- /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 an accepted 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 accepted agents and their current status. | + + +# **DeleteAgent** +> void DeleteAgent (Guid agentId) + +Delete an accepted 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 an accepted 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 an accepted 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 accepted agents and their current 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 accepted agents and their current 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 accepted agents and their current 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** | Accepted agents and their current status | - | +| **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..cd303abdd --- /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** | | [optional] +**Domains** | [**List<AgentDomainAdvertisement>**](AgentDomainAdvertisement.md) | | [optional] +**IsOnline** | **bool** | | +**LastSeenMs** | **long?** | | [optional] +**Name** | **string** | | +**RouteEpoch** | **long?** | | [optional] +**Subnets** | **List<string>** | | [optional] + +[[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/docs/PreflightOperation.md b/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md index ba5977cc9..01affec44 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/PreflightOperation.md @@ -10,7 +10,8 @@ Name | Type | Description | Notes **Kind** | **PreflightOperationKind** | | **ProxyCredential** | [**AppCredential**](AppCredential.md) | | [optional] **TargetCredential** | [**AppCredential**](AppCredential.md) | | [optional] -**TimeToLive** | **int?** | Retention duration in seconds for data provisioned by this operation. For \"provision-credentials\", this is the maximum staging time before the first credential checkout. After checkout, Gateway retains the credentials for later connections authorized for the same association. Optional parameter for \"provision-token\", \"provision-credentials\", and \"provision-connection-options\" kinds. | [optional] +**TimeToLive** | **int?** | Retention duration in seconds for data provisioned by this operation. For \"provision-credentials\", this is the maximum staging time before the first credential checkout. After checkout, Gateway retains the credentials for later connections authorized for the same association. Optional parameter for \"provision-token\", \"provision-credentials\", and \"provision-connection-options\" kinds. | [optional] **Token** | **string** | The token to be stored on the proxy-side. Required for \"provision-token\", \"provision-credentials\", and \"provision-connection-options\" kinds. | [optional] [[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..928dd66bc --- /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 an accepted agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// + void DeleteAgent(Guid agentId); + + /// + /// Delete an accepted 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 accepted agents and their current status. + /// + /// Thrown when fails to make API call + /// List<AgentInfo> + List ListAgents(); + + /// + /// List accepted agents and their current 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 an accepted 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 an accepted 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 accepted agents and their current 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 accepted agents and their current 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 an accepted agent by ID. + /// + /// Thrown when fails to make API call + /// Agent ID + /// + public void DeleteAgent(Guid agentId) + { + DeleteAgentWithHttpInfo(agentId); + } + + /// + /// Delete an accepted 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 an accepted 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 an accepted 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 accepted agents and their current status. + /// + /// Thrown when fails to make API call + /// List<AgentInfo> + public List ListAgents() + { + Devolutions.Gateway.Client.Client.ApiResponse> localVarResponse = ListAgentsWithHttpInfo(); + return localVarResponse.Data; + } + + /// + /// List accepted agents and their current 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 accepted agents and their current 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 accepted agents and their current 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..3d4d03db1 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentInfo.cs @@ -0,0 +1,169 @@ +/* + * 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. + /// domains. + /// isOnline (required). + /// lastSeenMs. + /// name (required). + /// routeEpoch. + /// subnets. + 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; + this.IsOnline = isOnline; + // 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.CertFingerprint = certFingerprint; + this.Domains = domains; + this.LastSeenMs = lastSeenMs; + this.RouteEpoch = routeEpoch; + 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", EmitDefaultValue = true)] + public string CertFingerprint { get; set; } + + /// + /// Gets or Sets Domains + /// + [DataMember(Name = "domains", 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", 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", EmitDefaultValue = true)] + public long? RouteEpoch { get; set; } + + /// + /// Gets or Sets Subnets + /// + [DataMember(Name = "subnets", 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 008e0d910..86f69c090 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 accepted agents and their current status. + operationId: ListAgents + responses: + '200': + description: Accepted agents and their current status + 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 an accepted 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 + - is_online + properties: + agent_id: + type: string + format: uuid + cert_fingerprint: + type: string + nullable: true + domains: + type: array + items: + $ref: '#/components/schemas/AgentDomainAdvertisement' + nullable: true + is_online: + type: boolean + last_seen_ms: + type: integer + format: int64 + nullable: true + minimum: 0 + name: + type: string + route_epoch: + type: integer + format: int64 + nullable: true + minimum: 0 + subnets: + type: array + items: + type: string + nullable: true 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: @@ -2255,6 +2465,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..04ed13cda --- /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 an accepted 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 accepted agents and their current 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..adfa2fd55 --- /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 | null; + domains?: Array | null; + is_online: boolean; + last_seen_ms?: number | null; + name: string; + route_epoch?: number | null; + subnets?: Array | null; +} + 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 ffd7270d6..be9b4cd02 100644 --- a/devolutions-gateway/src/api/tunnel.rs +++ b/devolutions-gateway/src/api/tunnel.rs @@ -10,6 +10,7 @@ use crate::http::HttpError; use crate::token::EnrollmentTokenClaims; #[derive(Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct EnrollRequest { /// Agent-generated UUID (the agent owns its identity). pub agent_id: Uuid, @@ -21,6 +22,7 @@ pub struct EnrollRequest { } #[derive(Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct EnrollResponse { /// Assigned agent ID. pub agent_id: Uuid, @@ -36,6 +38,7 @@ pub struct EnrollResponse { } #[derive(Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct AgentDomainAdvertisement { /// Domain route advertised by the Agent. pub domain: String, @@ -55,6 +58,7 @@ pub enum AgentStatus { } #[derive(Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct AgentInfo { /// Stable Agent identity. pub agent_id: Uuid, @@ -85,7 +89,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, @@ -242,7 +261,20 @@ fn agent_info( } /// List accepted agents and their current 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 = "Accepted agents and their current status", 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, @@ -266,7 +298,24 @@ async fn list_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, .. @@ -288,7 +337,24 @@ async fn get_agent( } /// Delete an accepted 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 151804a9e..2a8d57b42 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(), + ), + ); } } From 27baa3e9076d88189cef7e8308605775d608f12e Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Fri, 28 Aug 2026 13:25:15 -0400 Subject: [PATCH 7/8] fix(dgw): preserve agent tunnel default port Default the PowerShell Agent Tunnel configuration to UDP 4433 so an empty configuration remains valid after a Get/Set round trip. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DevolutionsGateway/Public/DGateway.ps1 | 2 +- powershell/pester/Config.Tests.ps1 | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/powershell/DevolutionsGateway/Public/DGateway.ps1 b/powershell/DevolutionsGateway/Public/DGateway.ps1 index 4b0cb1b82..435cba65c 100644 --- a/powershell/DevolutionsGateway/Public/DGateway.ps1 +++ b/powershell/DevolutionsGateway/Public/DGateway.ps1 @@ -283,7 +283,7 @@ function New-DGatewayWebAppConfig() { class DGatewayAgentTunnelConfig { [ValidateRange(1, 65535)] - [System.UInt16] $ListenPort + [System.UInt16] $ListenPort = 4433 DGatewayAgentTunnelConfig() { } diff --git a/powershell/pester/Config.Tests.ps1 b/powershell/pester/Config.Tests.ps1 index 4ca2dc046..9b03dced3 100644 --- a/powershell/pester/Config.Tests.ps1 +++ b/powershell/pester/Config.Tests.ps1 @@ -148,6 +148,22 @@ Describe 'Devolutions Gateway config' { { New-DGatewayAgentTunnelConfig -ListenPort 0 } | Should -Throw } + It 'Preserves the default agent tunnel port when updating an empty configuration' { + $EmptyAgentTunnelConfigPath = Join-Path $TestDrive 'EmptyAgentTunnel' + New-Item -Path $EmptyAgentTunnelConfigPath -ItemType 'Directory' | Out-Null + $ConfigFile = Join-Path $EmptyAgentTunnelConfigPath $DGatewayConfigFileName + [System.IO.File]::WriteAllText( + $ConfigFile, + '{"Hostname":"gateway.local","AgentTunnel":{}}', + $(New-Object System.Text.UTF8Encoding $False) + ) + + Set-DGatewayConfig -ConfigPath:$EmptyAgentTunnelConfigPath -Hostname 'updated.gateway.local' + + $SavedConfig = Get-Content -Path $ConfigFile -Encoding UTF8 | ConvertFrom-Json + $SavedConfig.AgentTunnel.ListenPort | Should -Be 4433 + } + It 'Sets basic standalone configuration' { $Hostname = "localhost" $HttpListener = New-DGatewayListener 'http://*:7172' 'http://*:7172' From 7050b5000f46051290aea00132e29da7e3b69019 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Fri, 28 Aug 2026 16:21:10 -0400 Subject: [PATCH 8/8] feat(dgw): publish agent status model Publish the stable Agent status enum and regenerate the OpenAPI documentation and .NET and TypeScript clients. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- devolutions-gateway/openapi/doc/index.adoc | 56 ++++++++------ .../dotnet-client/.openapi-generator/FILES | 2 + .../openapi/dotnet-client/README.md | 1 + .../docs/AgentDomainAdvertisement.md | 4 +- .../openapi/dotnet-client/docs/AgentInfo.md | 14 ++-- .../openapi/dotnet-client/docs/AgentStatus.md | 9 +++ .../Model/AgentDomainAdvertisement.cs | 10 ++- .../Model/AgentInfo.cs | 69 ++++++----------- .../Model/AgentStatus.cs | 76 +++++++++++++++++++ devolutions-gateway/openapi/gateway-api.yaml | 27 ++++--- .../.openapi-generator/FILES | 1 + .../model/agentDomainAdvertisement.ts | 6 ++ .../ts-angular-client/model/agentInfo.ts | 23 +++++- .../ts-angular-client/model/agentStatus.ts | 19 +++++ .../openapi/ts-angular-client/model/models.ts | 1 + devolutions-gateway/src/api/tunnel.rs | 1 + devolutions-gateway/src/openapi.rs | 1 + 17 files changed, 223 insertions(+), 97 deletions(-) create mode 100644 devolutions-gateway/openapi/dotnet-client/docs/AgentStatus.md create mode 100644 devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentStatus.cs create mode 100644 devolutions-gateway/openapi/ts-angular-client/model/agentStatus.ts diff --git a/devolutions-gateway/openapi/doc/index.adoc b/devolutions-gateway/openapi/doc/index.adoc index 3f65dcb5f..dd21d682a 100644 --- a/devolutions-gateway/openapi/doc/index.adoc +++ b/devolutions-gateway/openapi/doc/index.adoc @@ -3453,14 +3453,14 @@ endif::internal-generation[] | X | | Boolean -| +| Whether the Agent discovered the domain automatically. | | domain | X | | String -| +| Domain route advertised by the Agent. | |=== @@ -3482,62 +3482,68 @@ endif::internal-generation[] | X | | UUID -| +| Stable Agent identity. | uuid -| cert_fingerprint -| -| X -| String -| -| - | domains | | X | List of <> -| -| - -| is_online -| X -| -| Boolean -| +| Domain routes currently advertised by the Agent. | | last_seen_ms | | X | Long -| +| Last heartbeat timestamp in milliseconds since the Unix epoch. | int64 | name | X | | String -| +| Unique management name assigned during enrollment. | -| route_epoch -| +| status | X -| Long | -| int64 +| <> +| +| offline, online, unresponsive, | subnets | | X | List of <> -| +| Subnet routes currently advertised by the Agent. | |=== +[#AgentStatus] +=== _AgentStatus_ + + + + + + +[.fields-AgentStatus] +[cols="1"] +|=== +| Enum Values + +| offline +| online +| unresponsive + +|=== + + [#AppCredential] === _AppCredential_ diff --git a/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES b/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES index 0968dae76..b49a09154 100644 --- a/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES +++ b/devolutions-gateway/openapi/dotnet-client/.openapi-generator/FILES @@ -8,6 +8,7 @@ docs/AddressFamily.md docs/AgentApi.md docs/AgentDomainAdvertisement.md docs/AgentInfo.md +docs/AgentStatus.md docs/AppCredential.md docs/AppCredentialKind.md docs/AppTokenContentType.md @@ -115,6 +116,7 @@ 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/AgentStatus.cs src/Devolutions.Gateway.Client/Model/AppCredential.cs src/Devolutions.Gateway.Client/Model/AppCredentialKind.cs src/Devolutions.Gateway.Client/Model/AppTokenContentType.cs diff --git a/devolutions-gateway/openapi/dotnet-client/README.md b/devolutions-gateway/openapi/dotnet-client/README.md index 8712100cc..fcdf3979f 100644 --- a/devolutions-gateway/openapi/dotnet-client/README.md +++ b/devolutions-gateway/openapi/dotnet-client/README.md @@ -185,6 +185,7 @@ Class | Method | HTTP request | Description - [Model.AddressFamily](docs/AddressFamily.md) - [Model.AgentDomainAdvertisement](docs/AgentDomainAdvertisement.md) - [Model.AgentInfo](docs/AgentInfo.md) + - [Model.AgentStatus](docs/AgentStatus.md) - [Model.AppCredential](docs/AppCredential.md) - [Model.AppCredentialKind](docs/AppCredentialKind.md) - [Model.AppTokenContentType](docs/AppTokenContentType.md) diff --git a/devolutions-gateway/openapi/dotnet-client/docs/AgentDomainAdvertisement.md b/devolutions-gateway/openapi/dotnet-client/docs/AgentDomainAdvertisement.md index 9127b7807..7cba70cf9 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/AgentDomainAdvertisement.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/AgentDomainAdvertisement.md @@ -4,8 +4,8 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**AutoDetected** | **bool** | | -**Domain** | **string** | | +**AutoDetected** | **bool** | Whether the Agent discovered the domain automatically. | +**Domain** | **string** | Domain route advertised by 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/AgentInfo.md b/devolutions-gateway/openapi/dotnet-client/docs/AgentInfo.md index cd303abdd..902a16025 100644 --- a/devolutions-gateway/openapi/dotnet-client/docs/AgentInfo.md +++ b/devolutions-gateway/openapi/dotnet-client/docs/AgentInfo.md @@ -4,14 +4,12 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**AgentId** | **Guid** | | -**CertFingerprint** | **string** | | [optional] -**Domains** | [**List<AgentDomainAdvertisement>**](AgentDomainAdvertisement.md) | | [optional] -**IsOnline** | **bool** | | -**LastSeenMs** | **long?** | | [optional] -**Name** | **string** | | -**RouteEpoch** | **long?** | | [optional] -**Subnets** | **List<string>** | | [optional] +**AgentId** | **Guid** | Stable Agent identity. | +**Domains** | [**List<AgentDomainAdvertisement>**](AgentDomainAdvertisement.md) | Domain routes currently advertised by the Agent. | [optional] +**LastSeenMs** | **long?** | Last heartbeat timestamp in milliseconds since the Unix epoch. | [optional] +**Name** | **string** | Unique management name assigned during enrollment. | +**Status** | **AgentStatus** | | +**Subnets** | **List<string>** | Subnet routes currently advertised by the Agent. | [optional] [[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/AgentStatus.md b/devolutions-gateway/openapi/dotnet-client/docs/AgentStatus.md new file mode 100644 index 000000000..832868d4a --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/docs/AgentStatus.md @@ -0,0 +1,9 @@ +# Devolutions.Gateway.Client.Model.AgentStatus + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[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/Model/AgentDomainAdvertisement.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentDomainAdvertisement.cs index 4391bf48e..fe66c8372 100644 --- 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 @@ -41,8 +41,8 @@ protected AgentDomainAdvertisement() { } /// /// Initializes a new instance of the class. /// - /// autoDetected (required). - /// domain (required). + /// Whether the Agent discovered the domain automatically. (required). + /// Domain route advertised by the Agent. (required). public AgentDomainAdvertisement(bool autoDetected = default(bool), string domain = default(string)) { this.AutoDetected = autoDetected; @@ -55,14 +55,16 @@ protected AgentDomainAdvertisement() { } } /// - /// Gets or Sets AutoDetected + /// Whether the Agent discovered the domain automatically. /// + /// Whether the Agent discovered the domain automatically. [DataMember(Name = "auto_detected", IsRequired = true, EmitDefaultValue = true)] public bool AutoDetected { get; set; } /// - /// Gets or Sets Domain + /// Domain route advertised by the Agent. /// + /// Domain route advertised by the Agent. [DataMember(Name = "domain", IsRequired = true, EmitDefaultValue = true)] public string Domain { get; set; } 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 index 3d4d03db1..e5b6c2faa 100644 --- 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 @@ -33,6 +33,12 @@ namespace Devolutions.Gateway.Client.Model [DataContract(Name = "AgentInfo")] public partial class AgentInfo : IValidatableObject { + + /// + /// Gets or Sets Status + /// + [DataMember(Name = "status", IsRequired = true, EmitDefaultValue = true)] + public AgentStatus Status { get; set; } /// /// Initializes a new instance of the class. /// @@ -41,76 +47,59 @@ protected AgentInfo() { } /// /// Initializes a new instance of the class. /// - /// agentId (required). - /// certFingerprint. - /// domains. - /// isOnline (required). - /// lastSeenMs. - /// name (required). - /// routeEpoch. - /// subnets. - 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)) + /// Stable Agent identity. (required). + /// Domain routes currently advertised by the Agent.. + /// Last heartbeat timestamp in milliseconds since the Unix epoch.. + /// Unique management name assigned during enrollment. (required). + /// status (required). + /// Subnet routes currently advertised by the Agent.. + public AgentInfo(Guid agentId = default(Guid), List domains = default(List), long? lastSeenMs = default(long?), string name = default(string), AgentStatus status = default(AgentStatus), List subnets = default(List)) { this.AgentId = agentId; - this.IsOnline = isOnline; // 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.CertFingerprint = certFingerprint; + this.Status = status; this.Domains = domains; this.LastSeenMs = lastSeenMs; - this.RouteEpoch = routeEpoch; this.Subnets = subnets; } /// - /// Gets or Sets AgentId + /// Stable Agent identity. /// + /// Stable Agent identity. [DataMember(Name = "agent_id", IsRequired = true, EmitDefaultValue = true)] public Guid AgentId { get; set; } /// - /// Gets or Sets CertFingerprint - /// - [DataMember(Name = "cert_fingerprint", EmitDefaultValue = true)] - public string CertFingerprint { get; set; } - - /// - /// Gets or Sets Domains + /// Domain routes currently advertised by the Agent. /// + /// Domain routes currently advertised by the Agent. [DataMember(Name = "domains", 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 + /// Last heartbeat timestamp in milliseconds since the Unix epoch. /// + /// Last heartbeat timestamp in milliseconds since the Unix epoch. [DataMember(Name = "last_seen_ms", EmitDefaultValue = true)] public long? LastSeenMs { get; set; } /// - /// Gets or Sets Name + /// Unique management name assigned during enrollment. /// + /// Unique management name assigned during enrollment. [DataMember(Name = "name", IsRequired = true, EmitDefaultValue = true)] public string Name { get; set; } /// - /// Gets or Sets RouteEpoch - /// - [DataMember(Name = "route_epoch", EmitDefaultValue = true)] - public long? RouteEpoch { get; set; } - - /// - /// Gets or Sets Subnets + /// Subnet routes currently advertised by the Agent. /// + /// Subnet routes currently advertised by the Agent. [DataMember(Name = "subnets", EmitDefaultValue = true)] public List Subnets { get; set; } @@ -123,12 +112,10 @@ 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(" Status: ").Append(Status).Append("\n"); sb.Append(" Subnets: ").Append(Subnets).Append("\n"); sb.Append("}\n"); return sb.ToString(); @@ -156,12 +143,6 @@ IEnumerable IValidatableObject.Validate(ValidationContext vali 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/AgentStatus.cs b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentStatus.cs new file mode 100644 index 000000000..dcbe4e2a2 --- /dev/null +++ b/devolutions-gateway/openapi/dotnet-client/src/Devolutions.Gateway.Client/Model/AgentStatus.cs @@ -0,0 +1,76 @@ +/* + * 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 +{ + /// + /// Defines AgentStatus + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum AgentStatus + { + /// + /// Enum Offline for value: offline + /// + [EnumMember(Value = "offline")] + Offline = 1, + + /// + /// Enum Online for value: online + /// + [EnumMember(Value = "online")] + Online = 2, + + /// + /// Enum Unresponsive for value: unresponsive + /// + [EnumMember(Value = "unresponsive")] + Unresponsive = 3 + } + + public static class AgentStatusExtensions + { + /// + /// Returns the value as string for a given variant + /// + public static string ToValue(this AgentStatus variant) + { + switch (variant) + { + case AgentStatus.Offline: + return "offline"; + case AgentStatus.Online: + return "online"; + case AgentStatus.Unresponsive: + return "unresponsive"; + default: + throw new ArgumentOutOfRangeException(nameof(variant), $"Unexpected variant: {variant}"); + } + } + } + +} diff --git a/devolutions-gateway/openapi/gateway-api.yaml b/devolutions-gateway/openapi/gateway-api.yaml index 86f69c090..9c582d62d 100644 --- a/devolutions-gateway/openapi/gateway-api.yaml +++ b/devolutions-gateway/openapi/gateway-api.yaml @@ -1343,45 +1343,50 @@ components: properties: auto_detected: type: boolean + description: Whether the Agent discovered the domain automatically. domain: type: string + description: Domain route advertised by the Agent. AgentInfo: type: object required: - agent_id - name - - is_online + - status properties: agent_id: type: string format: uuid - cert_fingerprint: - type: string - nullable: true + description: Stable Agent identity. domains: type: array items: $ref: '#/components/schemas/AgentDomainAdvertisement' + description: Domain routes currently advertised by the Agent. nullable: true - is_online: - type: boolean last_seen_ms: type: integer format: int64 + description: Last heartbeat timestamp in milliseconds since the Unix epoch. nullable: true minimum: 0 name: type: string - route_epoch: - type: integer - format: int64 - nullable: true - minimum: 0 + description: Unique management name assigned during enrollment. + status: + $ref: '#/components/schemas/AgentStatus' subnets: type: array items: type: string + description: Subnet routes currently advertised by the Agent. nullable: true + AgentStatus: + type: string + enum: + - offline + - online + - unresponsive AppCredential: type: object required: diff --git a/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES b/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES index 42919a637..84eba61ed 100644 --- a/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES +++ b/devolutions-gateway/openapi/ts-angular-client/.openapi-generator/FILES @@ -25,6 +25,7 @@ model/ackResponse.ts model/addressFamily.ts model/agentDomainAdvertisement.ts model/agentInfo.ts +model/agentStatus.ts model/appCredential.ts model/appCredentialKind.ts model/appTokenContentType.ts diff --git a/devolutions-gateway/openapi/ts-angular-client/model/agentDomainAdvertisement.ts b/devolutions-gateway/openapi/ts-angular-client/model/agentDomainAdvertisement.ts index ca24a7fbd..6d98032e1 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/agentDomainAdvertisement.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/agentDomainAdvertisement.ts @@ -10,7 +10,13 @@ export interface AgentDomainAdvertisement { + /** + * Whether the Agent discovered the domain automatically. + */ auto_detected: boolean; + /** + * Domain route advertised by the Agent. + */ domain: string; } diff --git a/devolutions-gateway/openapi/ts-angular-client/model/agentInfo.ts b/devolutions-gateway/openapi/ts-angular-client/model/agentInfo.ts index adfa2fd55..73d5b746a 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/agentInfo.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/agentInfo.ts @@ -8,16 +8,33 @@ * Do not edit the class manually. */ import { AgentDomainAdvertisement } from './agentDomainAdvertisement'; +import { AgentStatus } from './agentStatus'; export interface AgentInfo { + /** + * Stable Agent identity. + */ agent_id: string; - cert_fingerprint?: string | null; + /** + * Domain routes currently advertised by the Agent. + */ domains?: Array | null; - is_online: boolean; + /** + * Last heartbeat timestamp in milliseconds since the Unix epoch. + */ last_seen_ms?: number | null; + /** + * Unique management name assigned during enrollment. + */ name: string; - route_epoch?: number | null; + status: AgentStatus; + /** + * Subnet routes currently advertised by the Agent. + */ subnets?: Array | null; } +export namespace AgentInfo { +} + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/agentStatus.ts b/devolutions-gateway/openapi/ts-angular-client/model/agentStatus.ts new file mode 100644 index 000000000..25ea00a9b --- /dev/null +++ b/devolutions-gateway/openapi/ts-angular-client/model/agentStatus.ts @@ -0,0 +1,19 @@ +/** + * 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 type AgentStatus = 'offline' | 'online' | 'unresponsive'; + +export const AgentStatus = { + Offline: 'offline' as AgentStatus, + Online: 'online' as AgentStatus, + Unresponsive: 'unresponsive' as AgentStatus +}; + diff --git a/devolutions-gateway/openapi/ts-angular-client/model/models.ts b/devolutions-gateway/openapi/ts-angular-client/model/models.ts index 4675ea6a7..7d7eb4f0f 100644 --- a/devolutions-gateway/openapi/ts-angular-client/model/models.ts +++ b/devolutions-gateway/openapi/ts-angular-client/model/models.ts @@ -4,6 +4,7 @@ export * from './ackResponse'; export * from './addressFamily'; export * from './agentDomainAdvertisement'; export * from './agentInfo'; +export * from './agentStatus'; export * from './appCredential'; export * from './appCredentialKind'; export * from './appTokenContentType'; diff --git a/devolutions-gateway/src/api/tunnel.rs b/devolutions-gateway/src/api/tunnel.rs index be9b4cd02..5fe1662f3 100644 --- a/devolutions-gateway/src/api/tunnel.rs +++ b/devolutions-gateway/src/api/tunnel.rs @@ -47,6 +47,7 @@ pub struct AgentDomainAdvertisement { } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "snake_case")] pub enum AgentStatus { /// No tunnel connection exists for the Agent. diff --git a/devolutions-gateway/src/openapi.rs b/devolutions-gateway/src/openapi.rs index 2a8d57b42..5052f6704 100644 --- a/devolutions-gateway/src/openapi.rs +++ b/devolutions-gateway/src/openapi.rs @@ -106,6 +106,7 @@ use crate::config::dto::{DataEncoding, PubKeyFormat, Subscriber}; crate::api::tunnel::EnrollRequest, crate::api::tunnel::EnrollResponse, crate::api::tunnel::AgentDomainAdvertisement, + crate::api::tunnel::AgentStatus, crate::api::tunnel::AgentInfo, )), modifiers(&SecurityAddon),