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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
# Changelog

## [1.0.3]

### Fixed

- **`VaultHttp.PostAsync`: JSON double-encoding caused HTTP 400 from Vault/OpenBao**

`PostAsync` manually serialized the request body to a JSON string using `JsonSerializer.Serialize`, then passed that string to `request.AddJsonBody()`. In RestSharp ≥ 106, `AddJsonBody` re-serializes whatever object it receives — when the argument is a `string`, it encodes it as a JSON string literal, wrapping the content in quotes and escaping the inner characters. Vault/OpenBao received `"{\\"csr\\":\\"...\\"}"` (a JSON-encoded string) instead of `{"csr":"..."}` (a JSON object) and returned HTTP 400 "error parsing JSON".

Fixed by replacing `request.AddJsonBody(serializedParams)` with `request.AddStringBody(serializedParams, ContentType.Json)`. `AddStringBody` sends the string as the raw request body without re-encoding it.

- **`ThrowOnAnyError = true` made the `BadRequest` error-parsing block dead code**

`RestClientOptions` was constructed with `ThrowOnAnyError = true`, which causes RestSharp to throw an exception on any non-2xx response before returning to the caller. The `PostAsync` method had explicit handling for `HttpStatusCode.BadRequest` that deserialized Vault error messages and threw a descriptive exception — but that block was never reached because RestSharp threw first, and the actual Vault error body was lost.

Fixed by removing `ThrowOnAnyError = true` from `RestClientOptions` and adding `response.ThrowIfError()` after the explicit `BadRequest` handler, so non-2xx responses that are not `BadRequest` still surface as exceptions while `BadRequest` responses are handled with full Vault error body parsing.

- **`ValidateCAConnectionInfo` and `ValidateProductInfo`: `KeyNotFoundException` on gateway config PUT/POST**

Both validation methods used direct `Dictionary.get_Item` indexers (`connectionInfo[key]`) to read parameters. The gateway does not always pre-populate every parameter key before calling validation, so any absent key threw `KeyNotFoundException` and surfaced as an opaque HTTP 500 from the gateway config endpoint.

Fixed by replacing all direct indexers with `TryGetValue` calls throughout both methods.

Additionally relaxed the `RoleName` requirement in `ValidateProductInfo`: the `Enroll` path already falls back to `ProductID` when `RoleName` is absent, so the validator no longer rejects configurations that omit it. The check now only errors if `RoleName` is explicitly present but empty.

### Changed

- Dropped .NET 6.0 target (EOL). The project now targets `net8.0` and `net10.0`.
## 1.0.2
* bug fix: _certDataReader is now initialized in the Initialize method

## 1.0.1
* added retrieval of roles associated with enrolled certificates via metadata for Vault Enterprise users

## 1.0.0
* initial release
* initial release
6 changes: 4 additions & 2 deletions hashicorp-vault-cagateway/Client/VaultHttp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public VaultHttp(string host, string mountPoint, string authToken, string nameSp
PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace
};

var restClientOptions = new RestClientOptions($"{host.TrimEnd('/')}/v1") { ThrowOnAnyError = true };
var restClientOptions = new RestClientOptions($"{host.TrimEnd('/')}/v1");
_restClient = new RestClient(restClientOptions, configureSerialization: s => s.UseSystemTextJson(_serializerOptions));

_mountPoint = mountPoint.TrimStart('/').TrimEnd('/'); // remove leading and trailing slashes
Expand Down Expand Up @@ -124,7 +124,7 @@ public async Task<T> PostAsync<T>(string path, dynamic parameters = default)
{
string serializedParams = JsonSerializer.Serialize(parameters);
logger.LogTrace($"serialized parameters (from {parameters.GetType()?.Name}): {serializedParams}");
request.AddJsonBody(serializedParams);
request.AddStringBody(serializedParams, ContentType.Json);
}

logger.LogTrace($"full url for the request: {_restClient.Options.BaseUrl}/{request.Resource}");
Expand All @@ -150,6 +150,8 @@ public async Task<T> PostAsync<T>(string path, dynamic parameters = default)
logger.LogTrace($"errors: {allErrors}");
throw new Exception(allErrors);
}

response.ThrowIfError();
return response.Data;
}
catch (Exception ex)
Expand Down
21 changes: 14 additions & 7 deletions hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,22 +384,23 @@ public async Task ValidateCAConnectionInfo(Dictionary<string, object> connection
List<string> errors = new List<string>();

// then, we make sure required fields are defined..
if (string.IsNullOrEmpty(connectionInfo[Constants.CAConfig.HOST] as string))
connectionInfo.TryGetValue(Constants.CAConfig.HOST, out var hostVal);
if (string.IsNullOrEmpty(hostVal as string))
{
errors.Add($"The '{Constants.CAConfig.HOST}' is required.");
}

if (string.IsNullOrEmpty(connectionInfo[Constants.CAConfig.MOUNTPOINT] as string))
connectionInfo.TryGetValue(Constants.CAConfig.MOUNTPOINT, out var mountVal);
if (string.IsNullOrEmpty(mountVal as string))
{
errors.Add($"The '{Constants.CAConfig.MOUNTPOINT}' is required.");
}

// make sure an authentication mechanism is defined (either certificate or token)
var token = connectionInfo[Constants.CAConfig.TOKEN] as string;

//var cert = connectionInfo[Constants.CAConfig.CLIENTCERT] as string;

var cert = string.Empty; // temporary until client cert auth into vault is implemented
connectionInfo.TryGetValue(Constants.CAConfig.TOKEN, out var tokenVal);
connectionInfo.TryGetValue(Constants.CAConfig.CLIENTCERT, out var certVal);
var token = tokenVal as string;
var cert = certVal as string;

if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(cert))
{
Expand Down Expand Up @@ -489,6 +490,12 @@ public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary<st
logger.LogError(LogHandler.FlattenException(ex));
throw;
}
// RoleName is optional — if absent or empty, ProductID is used as the role name (see Enroll).
productInfo.ProductParameters.TryGetValue(Constants.TemplateConfig.ROLENAME, out var roleNameVal);
if (roleNameVal != null && string.IsNullOrEmpty(roleNameVal as string))
{
errors.Add($"The '{Constants.TemplateConfig.ROLENAME}' must not be empty if provided.");
}

// if any errors, throw
if (errors.Any())
Expand Down
6 changes: 3 additions & 3 deletions hashicorp-vault-cagateway/hashicorp-vault-caplugin.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<RootNamespace>Keyfactor.Extensions.CAPlugin.HashicorpVault</RootNamespace>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>warnings</Nullable>
<AssemblyName>HashicorpVaultCAPlugin</AssemblyName>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<CopyLocalLockFileAssemblies>True</CopyLocalLockFileAssemblies>
<LangVersion>12.0</LangVersion>
<AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
<AppendTargetFrameworkToOutputPath>True</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>False</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>

Expand All @@ -18,7 +18,7 @@
<Optimize>False</Optimize>
<Deterministic>True</Deterministic>
<BaseOutputPath>bin</BaseOutputPath>
<AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
<AppendTargetFrameworkToOutputPath>True</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>False</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>

Expand Down
4 changes: 2 additions & 2 deletions integration-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json",
"integration_type": "anyca-plugin",
"name": "Hashicorp Vault AnyCA REST Gateway Plugin",
"status": "prototype",
"support_level": "community",
"status": "production",
"support_level": "kf-supported",
"link_github": true,
"update_catalog": false,
"description": "Hashicorp Vault plugin for the AnyCA REST Gateway Framework",
Expand Down
Loading