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