fix(esg): structured JSON AuthConfiguration (matches entity doc + UI placeholders) with legacy colon fallback — keys containing ':' no longer misparse
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -591,10 +591,40 @@ public class ExternalSystemClient : IExternalSystemClient
|
||||
return;
|
||||
}
|
||||
|
||||
// A config whose trimmed value starts with '{' is the structured JSON form
|
||||
// promised by the entity doc-comment and the Central UI placeholders. Parse
|
||||
// it first; only a non-JSON value falls through to the legacy colon-split.
|
||||
var isJson = system.AuthConfiguration.TrimStart().StartsWith('{');
|
||||
Dictionary<string, string?>? jsonFields = null;
|
||||
var jsonParsed = isJson && TryParseJsonAuth(system.AuthConfiguration, out jsonFields);
|
||||
|
||||
switch (authType)
|
||||
{
|
||||
case "apikey":
|
||||
// Auth config format: "HeaderName:KeyValue" or just "KeyValue" (default header: X-API-Key)
|
||||
// JSON form: {"header"?: string, "key": string} (default header X-API-Key).
|
||||
if (isJson)
|
||||
{
|
||||
if (jsonParsed &&
|
||||
jsonFields!.TryGetValue("key", out var jsonKey) &&
|
||||
!string.IsNullOrEmpty(jsonKey))
|
||||
{
|
||||
var header = jsonFields.TryGetValue("header", out var h) && !string.IsNullOrEmpty(h)
|
||||
? h!
|
||||
: "X-API-Key";
|
||||
request.Headers.TryAddWithoutValidation(header, jsonKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Malformed JSON, or JSON with no usable "key": send without a
|
||||
// header and warn (the value is never logged). Never throw.
|
||||
_logger.LogWarning(
|
||||
"ApplyAuth: External system '{System}' AuthType 'apikey' AuthConfiguration is malformed JSON (expected {{\"header\"?:...,\"key\":...}}); request will be sent without an auth header.",
|
||||
system.Name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Legacy config format: "HeaderName:KeyValue" or just "KeyValue" (default header: X-API-Key)
|
||||
var parts = system.AuthConfiguration.Split(':', 2);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
@@ -607,7 +637,28 @@ public class ExternalSystemClient : IExternalSystemClient
|
||||
break;
|
||||
|
||||
case "basic":
|
||||
// Auth config format: "username:password"
|
||||
// JSON form: {"username": string, "password": string}.
|
||||
if (isJson)
|
||||
{
|
||||
if (jsonParsed &&
|
||||
jsonFields!.TryGetValue("username", out var jsonUser) &&
|
||||
!string.IsNullOrEmpty(jsonUser))
|
||||
{
|
||||
var password = jsonFields.TryGetValue("password", out var p) ? p ?? string.Empty : string.Empty;
|
||||
var jsonEncoded = Convert.ToBase64String(
|
||||
Encoding.UTF8.GetBytes($"{jsonUser}:{password}"));
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", jsonEncoded);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"ApplyAuth: External system '{System}' AuthType 'basic' AuthConfiguration is malformed JSON (expected {{\"username\":...,\"password\":...}}); request will be sent without an Authorization header.",
|
||||
system.Name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Legacy config format: "username:password"
|
||||
var basicParts = system.AuthConfiguration.Split(':', 2);
|
||||
if (basicParts.Length == 2)
|
||||
{
|
||||
@@ -639,6 +690,37 @@ public class ExternalSystemClient : IExternalSystemClient
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a structured JSON <c>AuthConfiguration</c> into a case-insensitive
|
||||
/// field map (e.g. <c>{"header":"X-Auth","key":"..."}</c> or
|
||||
/// <c>{"username":"u","password":"p"}</c>). Returns <c>false</c> on malformed
|
||||
/// JSON (<see cref="JsonException"/>) instead of throwing, so the caller can
|
||||
/// fall through to the "send without header" warning path. Values are never
|
||||
/// logged.
|
||||
/// </summary>
|
||||
private static bool TryParseJsonAuth(string config, out Dictionary<string, string?> fields)
|
||||
{
|
||||
try
|
||||
{
|
||||
fields = JsonSerializer.Deserialize<Dictionary<string, string?>>(
|
||||
config,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||
?? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Normalize to case-insensitive so "Header"/"header" both resolve.
|
||||
if (!ReferenceEquals(fields.Comparer, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
fields = new Dictionary<string, string?>(fields, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
fields = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(ExternalSystemDefinition? system, ExternalSystemMethod? method)> ResolveSystemAndMethodAsync(
|
||||
string systemName,
|
||||
string methodName,
|
||||
|
||||
Reference in New Issue
Block a user