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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
- Environment variable overrides are trimmed of leading/trailing whitespace before use.
- Hardened validation and logging around connection parameters and authentication failures.

## Fixes

- **Sanitized `ApiException` in secret retrieval** — a failed Akeyless secret lookup (e.g. HTTP error from `GetSecretValuesAsync`) previously let the raw SDK exception propagate to Keyfactor Command's logs. The SDK's `ApiException.Message` can echo back portions of the request, including the live auth token. Secret retrieval now catches `ApiException`, logs only the HTTP status code, and throws a sanitized `InvalidSecretConfigurationException`, matching the existing pattern in authentication (`InitClient`).

# v1.0.0

Initial release of the Akeyless PAM Provider for Keyfactor Command and Universal Orchestrator.
Expand Down
13 changes: 13 additions & 0 deletions akeyless-pam/AkeylessPam.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,19 @@ private async Task<string> GetStaticSecret(IAkeylessApiClient client, AkeylessCo
configurationInfo.SecretName, configurationInfo.SecretType);
return result;
}
catch (ApiException ex)
{
// NOTE: the exception object itself (not just ex.Message) is intentionally excluded from
// the log call — see the matching comment in InitClient. ApiException error content may
// echo back portions of the request, including the live auth token, and most ILogger
// providers render an attached exception's Message/ToString() regardless of the message
// template, so passing `ex` here would defeat that exclusion.
Logger.LogError(
"Akeyless API exception while retrieving secret '{SecretName}' (HTTP {StatusCode})",
configurationInfo.SecretName, ex.ErrorCode);
throw new InvalidSecretConfigurationException(
$"Unable to retrieve secret '{configurationInfo.SecretName}' from Akeyless (HTTP {ex.ErrorCode}).");
}
finally
{
Logger.MethodExit();
Expand Down
1 change: 1 addition & 0 deletions docsource/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Integration tests load credentials from environment variables. As a convenience
| `GetPassword_StaticJson_MissingField_ThrowsInvalidSecretConfigurationException` | JSON content, requested field absent → exception |
| `GetPassword_SecretNotInResponse_ThrowsInvalidSecretConfigurationException` | API response does not contain the requested secret name → exception |
| `GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationException` | Secret found but value is empty → exception |
| `GetPassword_GetSecretValuesThrowsApiException_WrapsAsInvalidSecretConfigurationException` | `GetSecretValuesAsync` throws `ApiException` → sanitized `InvalidSecretConfigurationException` (message excludes the raw SDK exception content) |

#### Configuration Model (`AkeylessConfigurationTests`)

Expand Down
18 changes: 18 additions & 0 deletions tests/AkeylessPam.Unit.Tests/AkeylessPamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,24 @@ public void GetPassword_StaticText_PlainString_ReturnsAsIs()
Assert.Equal("my-password", result);
}

[Fact]
public void GetPassword_GetSecretValuesThrowsApiException_WrapsAsInvalidSecretConfigurationException()
{
var mock = new Mock<IAkeylessApiClient>();
mock.Setup(c => c.Authenticate(It.IsAny<string>(), It.IsAny<string>()))
.Returns("fake-token");
mock.Setup(c => c.GetSecretValuesAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()))
.Throws(new ApiException(404, "Not Found: request body echo including fake-token"));

var pam = new AkeylessPam(_ => mock.Object);

var ex = Assert.Throws<AggregateException>(() =>
pam.GetPassword(Params.Instance(), Params.ValidServer()));

var inner = Assert.IsType<InvalidSecretConfigurationException>(ex.InnerException);
Assert.DoesNotContain("fake-token", inner.Message);
}

[Fact]
public void GetPassword_StaticText_JsonContent_ReturnsFullJsonBlob()
{
Expand Down
1 change: 1 addition & 0 deletions tests/AkeylessPam.Unit.Tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Tests for `AkeylessPam.GetPassword()` covering configuration validation, authent
| `GetPassword_StaticJson_MissingField_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when `StaticSecretFieldName` is not present in the JSON |
| `GetPassword_SecretNotInResponse_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when the API returns an empty dictionary (secret not found) |
| `GetPassword_EmptySecretValue_ThrowsInvalidSecretConfigurationException` | Throws `InvalidSecretConfigurationException` when the API returns an empty string for the secret value |
| `GetPassword_GetSecretValuesThrowsApiException_WrapsAsInvalidSecretConfigurationException` | Throws sanitized `InvalidSecretConfigurationException` when `GetSecretValuesAsync` throws `ApiException`; asserts the raw SDK exception content is not present in the resulting message |
| `GetPassword_StaticJson_WhitespaceFieldName_ReturnsFullBlob` | A `static_json` secret with a whitespace-only `StaticSecretFieldName` (e.g. a space from the Command UI) returns the full JSON blob |
| `GetPassword_StaticKv_JsonStoredAsKv_ParsesViaJson` | When a `static_kv` secret contains JSON instead of `key=value` lines, falls back to JSON parsing |

Expand Down
Loading