From 1ee7a73d09d2d8afdf23351cdfd0fb454d3c96a4 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Mon, 21 Sep 2026 20:53:21 -0400 Subject: [PATCH] Fix Enroll failures reporting INPROCESS instead of FAILED, surface flow summary in StatusMessage Every early-return failure path in Enroll (plus the catch-all exception handler) set Status = 30 (INPROCESS) instead of FAILED, so Command treated these as still-pending rather than errors and never surfaced them in the UI. Follow the same fix already proven on feature/ev-ov-dv-multiname-certs: use EndEntityStatus.FAILED throughout, and add FlowLogger.GetSummary() (a concise step list, distinct from the ASCII-tree RenderFlow() used for Trace logs) to prepend what the plugin actually attempted ahead of the terse error text. Also fixes the reissue "one click not available" message incorrectly saying "Renew" instead of "Reissue". --- cscglobal-caplugin/CSCGlobalCAPlugin.cs | 64 ++++++++++++------------- cscglobal-caplugin/FlowLogger.cs | 33 +++++++++++++ 2 files changed, 65 insertions(+), 32 deletions(-) diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs index 96bddaa..457ef42 100644 --- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs +++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs @@ -678,8 +678,8 @@ await flow.StepAsync("SubmitRegistrationToCSC", async () => flow.Fail("ParseResponse", "API returned null"); return new EnrollmentResult { - Status = 30, - StatusMessage = "Enrollment failed: CSC API returned a null response." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nEnrollment failed: CSC API returned a null response." }; } flow.Step("ParseResponse", $"error={enrollmentResponse.RegistrationError != null}"); @@ -690,8 +690,8 @@ await flow.StepAsync("SubmitRegistrationToCSC", async () => flow.Fail("RejectExpiredRenew", "PriorCertSN present on New enrollment"); return new EnrollmentResult { - Status = 30, - StatusMessage = "You cannot renew an expired cert please perform an new enrollment." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nYou cannot renew an expired cert please perform an new enrollment." }; } @@ -726,8 +726,8 @@ await flow.StepAsync("PollForIssuance", async () => flow.Fail("ValidatePriorSN", "PriorCertSN is empty"); return new EnrollmentResult { - Status = 30, - StatusMessage = "RenewOrReissue failed: PriorCertSN is required but was not provided." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nRenewOrReissue failed: PriorCertSN is required but was not provided." }; } @@ -742,8 +742,8 @@ await flow.StepAsync("LookupOrderId", async () => flow.Fail("ValidateOrderId", $"no order found for SN={priorSn}"); return new EnrollmentResult { - Status = 30, - StatusMessage = $"RenewOrReissue failed: could not find order ID for serial number '{priorSn}'." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nRenewOrReissue failed: could not find order ID for serial number '{priorSn}'." }; } @@ -752,8 +752,8 @@ await flow.StepAsync("LookupOrderId", async () => flow.Fail("ValidateOrderId", $"order_id too short ({order_id.Length} chars)"); return new EnrollmentResult { - Status = 30, - StatusMessage = $"RenewOrReissue failed: order ID '{order_id}' is too short to extract a UUID." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nRenewOrReissue failed: order ID '{order_id}' is too short to extract a UUID." }; } flow.Step("ValidateOrderId", $"orderId={order_id}"); @@ -801,8 +801,8 @@ await flow.StepAsync("FetchLiveCertForDecision", async () => flow.Fail("FallbackExpiryCheck", fallbackEx.Message); return new EnrollmentResult { - Status = 30, - StatusMessage = $"RenewOrReissue failed: unable to determine renewal status for order '{order_id}'. {fallbackEx.Message}" + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nRenewOrReissue failed: unable to determine renewal status for order '{order_id}'. {fallbackEx.Message}" }; } } @@ -825,8 +825,8 @@ await flow.StepAsync("LookupRenewalUUID", async () => flow.Fail("ValidateRenewalUUID", "could not resolve PriorCertSN"); return new EnrollmentResult { - Status = 30, - StatusMessage = "Renewal failed: could not resolve prior certificate serial number to a request ID." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nRenewal failed: could not resolve prior certificate serial number to a request ID." }; } flow.Step("ValidateRenewalUUID", $"uuid={uUId}"); @@ -850,8 +850,8 @@ await flow.StepAsync("SubmitRenewalToCSC", async () => flow.Fail("ParseRenewalResponse", "API returned null"); return new EnrollmentResult { - Status = 30, - StatusMessage = "Renewal failed: CSC API returned a null response." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nRenewal failed: CSC API returned a null response." }; } @@ -871,9 +871,9 @@ await flow.StepAsync("PollForIssuance", async () => flow.Fail("MissingEnrollmentParams", "Applicant Last Name not present — one-click renew unavailable"); return new EnrollmentResult { - Status = 30, + Status = (int)EndEntityStatus.FAILED, StatusMessage = - "One click Renew Is Not Available for this Certificate Type. Use the configure button instead." + $"{flow.GetSummary()}\n\nOne click Renew Is Not Available for this Certificate Type. Use the configure button instead." }; } @@ -892,8 +892,8 @@ await flow.StepAsync("LookupReissueRequestId", async () => flow.Fail("ValidateReissueRequestId", "could not resolve PriorCertSN"); return new EnrollmentResult { - Status = 30, - StatusMessage = "Reissue failed: could not resolve prior certificate serial number to a request ID." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nReissue failed: could not resolve prior certificate serial number to a request ID." }; } @@ -902,8 +902,8 @@ await flow.StepAsync("LookupReissueRequestId", async () => flow.Fail("ValidateReissueRequestId", $"requestid too short ({requestid.Length} chars)"); return new EnrollmentResult { - Status = 30, - StatusMessage = $"Reissue failed: request ID '{requestid}' is too short to extract a UUID." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nReissue failed: request ID '{requestid}' is too short to extract a UUID." }; } @@ -929,8 +929,8 @@ await flow.StepAsync("SubmitReissueToCSC", async () => flow.Fail("ParseReissueResponse", "API returned null"); return new EnrollmentResult { - Status = 30, - StatusMessage = "Reissue failed: CSC API returned a null response." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nReissue failed: CSC API returned a null response." }; } @@ -950,17 +950,17 @@ await flow.StepAsync("PollForIssuance", async () => flow.Fail("MissingEnrollmentParams", "Applicant Last Name not present — one-click reissue unavailable"); return new EnrollmentResult { - Status = 30, + Status = (int)EndEntityStatus.FAILED, StatusMessage = - "One click Renew Is Not Available for this Certificate Type. Use the configure button instead." + $"{flow.GetSummary()}\n\nOne click Reissue Is Not Available for this Certificate Type. Use the configure button instead." }; default: flow.Fail("UnhandledType", $"enrollmentType={enrollmentType}"); return new EnrollmentResult { - Status = 30, - StatusMessage = $"Enroll failed: unhandled enrollment type '{enrollmentType}'." + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nEnroll failed: unhandled enrollment type '{enrollmentType}'." }; } } @@ -971,8 +971,8 @@ await flow.StepAsync("PollForIssuance", async () => Logger.LogError(inner, "Enroll: AggregateException during {EnrollmentType}: {Message}", enrollmentType, inner?.Message ?? ae.Message); return new EnrollmentResult { - Status = 30, - StatusMessage = $"Enrollment failed with error: {inner?.Message ?? ae.Message}" + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nEnrollment failed with error: {inner?.Message ?? ae.Message}" }; } catch (Exception ex) @@ -981,8 +981,8 @@ await flow.StepAsync("PollForIssuance", async () => Logger.LogError(ex, "Enroll: unhandled exception during {EnrollmentType}: {Message}", enrollmentType, ex.Message); return new EnrollmentResult { - Status = 30, - StatusMessage = $"Enrollment failed with error: {ex.Message}" + Status = (int)EndEntityStatus.FAILED, + StatusMessage = $"{flow.GetSummary()}\n\nEnrollment failed with error: {ex.Message}" }; } } diff --git a/cscglobal-caplugin/FlowLogger.cs b/cscglobal-caplugin/FlowLogger.cs index 5696fcd..cd6718f 100644 --- a/cscglobal-caplugin/FlowLogger.cs +++ b/cscglobal-caplugin/FlowLogger.cs @@ -219,6 +219,39 @@ private string RenderFlow() return sb.ToString(); } + /// + /// Concise step-by-step summary suitable for surfacing in a user-facing failure message + /// (unlike 's ASCII-art tree, which is meant for Trace logs only). + /// + public string GetSummary() + { + var hasFailures = _steps.Any(s => s.Status == FlowStepStatus.Failed) || + _steps.SelectMany(s => s.Children).Any(c => c.Status == FlowStepStatus.Failed); + var overallStatus = hasFailures ? "FAILED" : "OK"; + + var sb = new StringBuilder(); + sb.AppendLine($"Flow: {_flowName} [{overallStatus}] Total: {_totalTimer.ElapsedMilliseconds}ms"); + sb.AppendLine("----------------------------------------"); + + foreach (var step in _steps) + { + AppendSummaryLine(sb, step, 0); + foreach (var child in step.Children) + AppendSummaryLine(sb, child, 1); + } + + return sb.ToString(); + } + + private static void AppendSummaryLine(StringBuilder sb, FlowStep step, int indentLevel) + { + var indent = new string(' ', indentLevel * 2); + var icon = GetStatusIcon(step.Status); + var elapsed = step.ElapsedMs > 0 ? $" ({step.ElapsedMs}ms)" : ""; + var detail = !string.IsNullOrEmpty(step.Detail) ? $" - {step.Detail}" : ""; + sb.AppendLine($"{indent}{icon} {step.Name}{elapsed}{detail}"); + } + private static string GetStatusIcon(FlowStepStatus status) { return status switch