Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f8e67eb
Reapply "dns validation"
bhillkeyfactor Aug 10, 2026
a74a262
Update HydrantIdCAPlugin.csproj
bhillkeyfactor Aug 10, 2026
97a2f77
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 10, 2026
ee41c62
Update CHANGELOG.md
bhillkeyfactor Aug 11, 2026
8c54d72
Update HydrantCAProxy.Tests.csproj
bhillkeyfactor Aug 11, 2026
a80b846
Update HydrantCAProxy.Tests.csproj
bhillkeyfactor Aug 11, 2026
9a74614
make domain validator optional on a policy, skip DCV when unset
bhillkeyfactor Aug 11, 2026
95e20ba
added logging
bhillkeyfactor Sep 1, 2026
9acb7d2
Addded Tests
bhillkeyfactor Sep 1, 2026
0bafe39
functional test plan
bhillkeyfactor Sep 1, 2026
41d0b09
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 1, 2026
f2641e6
fixed ping issue
bhillkeyfactor Sep 1, 2026
de8ab54
Merge branch 'dnspluginsupport' of https://github.com/Keyfactor/hydra…
bhillkeyfactor Sep 1, 2026
deea472
Added missing account id
bhillkeyfactor Sep 1, 2026
0369555
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 1, 2026
aedb7bf
org id
bhillkeyfactor Sep 1, 2026
f2edbc6
Merge branch 'dnspluginsupport' of https://github.com/Keyfactor/hydra…
bhillkeyfactor Sep 1, 2026
ec775e0
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 1, 2026
bbdaf8c
fixed domain validation
bhillkeyfactor Sep 1, 2026
5903906
Merge branch 'dnspluginsupport' of https://github.com/Keyfactor/hydra…
bhillkeyfactor Sep 1, 2026
510cc8e
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 1, 2026
2c9d679
added dns control
bhillkeyfactor Sep 2, 2026
7992d2d
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 2, 2026
3b349c4
fixed domain validation
bhillkeyfactor Sep 2, 2026
31a32e2
Merge branch 'dnspluginsupport' of https://github.com/Keyfactor/hydra…
bhillkeyfactor Sep 2, 2026
6ce6ab6
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 2, 2026
f80bdc0
fixed domains
bhillkeyfactor Sep 2, 2026
b2af184
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 2, 2026
a148042
debug
bhillkeyfactor Sep 2, 2026
d243c3b
fixed organizationid issue
bhillkeyfactor Sep 2, 2026
cafe871
fixed org link up
bhillkeyfactor Sep 3, 2026
e3118c2
Fixed Test Cases
bhillkeyfactor Sep 4, 2026
5379ce4
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 4, 2026
69c1c4e
push fixes
bhillkeyfactor Sep 8, 2026
263995d
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 8, 2026
9b6cf4c
fixed policy issue
bhillkeyfactor Sep 8, 2026
2992ea7
Merge branch 'dnspluginsupport' of https://github.com/Keyfactor/hydra…
bhillkeyfactor Sep 8, 2026
3482c0f
fixed docs
bhillkeyfactor Sep 9, 2026
1aab1c3
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 9, 2026
8819a09
fixed changelog
bhillkeyfactor Sep 10, 2026
e516d0d
Merge branch 'dnspluginsupport' of https://github.com/Keyfactor/hydra…
bhillkeyfactor Sep 10, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,5 @@ ASALocalRun/
.claude/settings.local.json
sample change.txt
.claude/settings.json
postman/*.postman_environment.local.json

30 changes: 29 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# v3.0.0
* Added a CertificateAuthorityId CA connection setting that scopes a logical CA to one certificate authority within the HydrantId tenant, matched against the certificateAuthorityId each policy reports on GET /api/v2/policies. HydrantId's policy and certificate endpoints are account-scoped rather than CA-scoped, so a tenant issuing from more than one CA previously had every logical CA defined against it offer every policy, synchronize every certificate, and be able to revoke any of them -- cross-contaminating each CA's inventory in Command. Blank preserves the previous unscoped behaviour for single-CA tenants
* Changed Synchronize to skip certificates whose policy belongs to another certificate authority, matching on the policy reference the certificate list already returns so a foreign certificate costs no detail fetch, and added filtered and error counts to the synchronization summary alongside the processed and skipped counts
* Changed GetProductIds to offer only the policies belonging to this CA, so a Command template cannot be mapped to a policy that issues from a different certificate authority
* Changed enrollment and re-issue to refuse a policy belonging to another certificate authority before submitting the request, rather than issuing a certificate from a CA the logical CA is not scoped to
* Fixed Revoke acting on any certificate in the tenant regardless of which certificate authority issued it: revocation now confirms the certificate was issued under one of this CA's policies first, and refuses when it was not or when its record cannot be read. Unscoped CAs are unaffected and pay no extra call
* Changed GetSingleRecord to refuse a certificate issued under another certificate authority's policy, naming the certificate's policy and the configured CA. The plugin's own post-enrollment and renewal lookups deliberately bypass the check, since enrollment has already verified the policy and re-checking would add a policy list round trip to every issuance
* Fixed a synchronization run whose CertificateAuthorityId scope could not be resolved leaving the Gateway's certificate buffer open, which stranded the sync job waiting on items that would never arrive instead of surfacing the error; the failure now completes the buffer on the way out like every other synchronization failure
* Added a warning when CertificateAuthorityId excludes every certificate in the account, listing the policies the CA owns against the policies seen on the excluded certificates, since the counts alone make that case indistinguishable from an empty CA
* Added validation of CertificateAuthorityId when the CA connection is saved, so a value matching no policy is reported on the config screen rather than at the first synchronization
* Added the policy id to the deserialized policy reference on certificate records, so CA scoping matches on the stable id where HydrantId supplies it and falls back to the policy name where it does not
* Fixed "No valid domains associated with organization" on POST /csr for a domain that was already VALIDATED at HydrantId but never linked to the enrolling policy's organization (e.g. validated before organizationIds was sent on creation, or under a different policy). The plugin now calls POST /domains/{id} to link or relink the organization at the moment validation is confirmed, instead of only logging a diagnostic warning
* Added automated DNS-01 style domain control validation: when the AnyCA Gateway supplies an IDomainValidatorFactory and a DNS provider plugin is configured for the domain's zone, the plugin now stages HydrantId's validation TXT record, polls until the domain is VALIDATED, removes the record, and issues the certificate within a single enrollment call
* Added the enrolling policy's organizationId to the domain validation request as organizationIds; HydrantId policies issue under an organization and POST /csr rejects domains that are not associated with it ("No valid domains associated with organization for IdenTrust policy"), and the plugin previously created every domain with a null organizationIds
* Changed domain control validation to target the registrable base domain rather than the CSR's fully-qualified name; HydrantId links the vetted organization to the base domain only, and validating a subdomain produced a record with a null organizationIds that POST /csr rejected with "No valid domains associated with organization". A base-domain validation additionally covers every subdomain until domainValidUntil
* Changed DNS provider plugin resolution to try the base domain and then the requested name, because the Gateway matches a domain validation configuration on exact domain equality; a configuration registered against either name now resolves, and the record is still written on the base domain
* Changed the DNS provider validation type tried first from "dns-01" to "DNS", which is what deployed DNS plugins report to AnyCA Gateway 26.2; the other spelling is still attempted as a fallback
* Added a fallback to the fully-qualified name when HydrantId will not accept the derived base domain, so an unrecognized multi-label public suffix costs one rejected API call rather than a failed enrollment
* Added a diagnostic that reports a validated domain's organizationIds, warning when it is empty, so the opaque "No valid domains associated with organization" failure from POST /csr is visible at the point domain validation completes
* Added per-domain fallback to external validation when automation is unavailable (no factory, no DNS plugin for the zone, staging failure, no validation code, or validation timeout), preserving the previous manual publish-and-resubmit behaviour
* Added DnsPropagationDelaySeconds, DomainValidationTimeoutSeconds and DomainValidationPollIntervalSeconds CA connection settings
* Added domain control validation for policies that declare a validator, including reuse of an already-validated parent domain for subdomains and regeneration of expired validation codes
* Added HydrantIdAccountId and the HydrantIdOrg*/contact CA connection settings required by validators (e.g. IdenTrust) that declare a non-empty requiredPayload
* Made the policy domain validator optional - policies with no validator configured skip domain control validation entirely
* Fixed soft-deleted HydrantId domain records (deletedAt) being matched during domain control validation; re-checking a deleted record returned HTTP 500 and failed the enrollment instead of starting a fresh validation
* Fixed the extension registration key in manifest.json, which was GCPCASCAPlugin copy-paste residue rather than HydrantIdCAPlugin
* Synchronized integration-manifest.json CA connection settings with the plugin annotations; HydrantIdAccountId and the organization fields were previously missing from the generated documentation

# v1.0.3
* Added support for revocation reason 0 (Unspecified) now that HydrantId accepts it
* Added support for revocation reason 0 (Unspecified) now that HydrantId accepts it
* Fixed sensitive credentials (HydrantIdAuthId, HydrantIdAuthKey) being written to trace logs in plain text; raw config JSON is now masked before logging

# v1.0.2
Expand Down
179 changes: 179 additions & 0 deletions HydrantCAProxy.Tests/FlowLoggerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2025 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Keyfactor.HydrantId;
using Microsoft.Extensions.Logging;
using Xunit;

namespace HydrantCAProxy.Tests
{
public class FlowLoggerTests
{
// A minimal ILogger test double that records the formatted message of every
// Log call, so assertions can check FlowLogger's actual output content instead
// of just "no exception was thrown."
private sealed class RecordingLogger : ILogger
{
public List<string> Messages { get; } = new List<string>();

public IDisposable BeginScope<TState>(TState state) => null;

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
Messages.Add(formatter(state, exception));
}
}

[Fact]
public void Constructor_NullLogger_Throws()
{
Assert.Throws<ArgumentNullException>(() => new FlowLogger(null, "Test"));
}

[Fact]
public void Constructor_NullFlowName_DefaultsToUnknown()
{
var logger = new RecordingLogger();

using var flow = new FlowLogger(logger, null);

Assert.Contains(logger.Messages, m => m.Contains("Unknown"));
}

[Fact]
public void Step_WithDetail_LogsDetail()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");

flow.Step("MyStep", "some detail");

Assert.Contains(logger.Messages, m => m.Contains("MyStep") && m.Contains("some detail"));
}

[Fact]
public void Step_NullDetail_LogsOk()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");

flow.Step("MyStep");

Assert.Contains(logger.Messages, m => m.Contains("MyStep") && m.Contains("OK"));
}

[Fact]
public void Step_ActionSucceeds_LogsOk()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");
var ran = false;

flow.Step("MyStep", () => { ran = true; });

Assert.True(ran);
Assert.Contains(logger.Messages, m => m.Contains("MyStep") && m.Contains("OK"));
}

[Fact]
public void Step_ActionThrows_LogsFailureAndRethrows()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");

var ex = Assert.Throws<InvalidOperationException>(() =>
flow.Step("MyStep", () => throw new InvalidOperationException("boom")));

Assert.Equal("boom", ex.Message);
Assert.Contains(logger.Messages, m => m.Contains("MyStep") && m.Contains("FAILED") && m.Contains("boom"));
}

[Fact]
public async Task StepAsync_ActionSucceeds_LogsOk()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");
var ran = false;

await flow.StepAsync("MyStep", async () =>
{
await Task.Delay(1);
ran = true;
});

Assert.True(ran);
Assert.Contains(logger.Messages, m => m.Contains("MyStep") && m.Contains("OK"));
}

[Fact]
public async Task StepAsync_ActionThrows_LogsFailureAndRethrows()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");

var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
flow.StepAsync("MyStep", () => throw new InvalidOperationException("async boom")));

Assert.Equal("async boom", ex.Message);
Assert.Contains(logger.Messages, m => m.Contains("MyStep") && m.Contains("FAILED") && m.Contains("async boom"));
}

[Fact]
public void Fail_LogsFailedWithReason()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");

flow.Fail("MyStep", "went wrong");

Assert.Contains(logger.Messages, m => m.Contains("MyStep") && m.Contains("FAILED") && m.Contains("went wrong"));
}

[Fact]
public void Skip_LogsSkippedWithReason()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");

flow.Skip("MyStep", "not needed");

Assert.Contains(logger.Messages, m => m.Contains("MyStep") && m.Contains("SKIPPED") && m.Contains("not needed"));
}

[Fact]
public void Dispose_NoFailures_LogsSuccessSummary()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");
flow.Step("Step1", "ok");
flow.Skip("Step2", "skipped");

flow.Dispose();

Assert.Contains(logger.Messages, m => m.Contains("FLOW DIAGRAM"));
Assert.Contains(logger.Messages, m => m.Contains("[OK]") && m.Contains("Step1"));
Assert.Contains(logger.Messages, m => m.Contains("[SKIP]") && m.Contains("Step2"));
Assert.Contains(logger.Messages, m => m.Contains("SUCCESS"));
}

[Fact]
public void Dispose_WithFailure_LogsPartialFailureSummary()
{
var logger = new RecordingLogger();
var flow = new FlowLogger(logger, "Test");
flow.Fail("Step1", "broke");

flow.Dispose();

Assert.Contains(logger.Messages, m => m.Contains("[FAIL]") && m.Contains("Step1"));
Assert.Contains(logger.Messages, m => m.Contains("PARTIAL FAILURE"));
}
}
}
3 changes: 2 additions & 1 deletion HydrantCAProxy.Tests/HydrantCAProxy.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
<IsPackable>false</IsPackable>
Expand All @@ -10,6 +10,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Loading
Loading