Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
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

20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# v1.1.0
* 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