Add six-month domain and broker monitoring

This commit is contained in:
2026-09-04 16:57:34 -06:00
parent f2a40f051b
commit dcbf5e87e3
20 changed files with 998 additions and 28 deletions
@@ -35,6 +35,10 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
string password,
CancellationToken cancellationToken)
{
using IDisposable? logScope = logger.BeginScope(
"InstitutionalUser={InstitutionalUser}; InstitutionalRole={InstitutionalRole}",
identity.UserName,
identity.Role);
Uri authenticationUri = new(
new Uri(options.Endpoint, UriKind.Absolute),
options.AuthenticationPath);
@@ -155,6 +159,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
}
logger.LogInformation(
BrokerEventIds.SguAuthenticationAccepted,
"SGU accepted credentials after an explicit NTLM challenge in {ElapsedMilliseconds} ms.",
elapsed.ElapsedMilliseconds);
return (null, continuationUri);
@@ -184,6 +189,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
logger.LogWarning(
BrokerEventIds.SguAuthenticationTimeout,
"SGU NTLM authentication timed out after {ElapsedMilliseconds} ms.",
elapsed.ElapsedMilliseconds);
return (NtlmValidationResult.Unavailable("NTLM_TIMEOUT"), null);
@@ -191,6 +197,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
catch (HttpRequestException exception)
{
logger.LogWarning(
BrokerEventIds.SguAuthenticationNetworkFailure,
exception,
"SGU NTLM authentication failed after {ElapsedMilliseconds} ms.",
elapsed.ElapsedMilliseconds);
@@ -347,6 +354,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
if (profile is null)
{
logger.LogWarning(
BrokerEventIds.ProfileHtmlUnexpected,
"SGU returned a profile page for role {Role}, but no supported profile fields were found after {ElapsedMilliseconds} ms.",
identity.Role,
elapsed.ElapsedMilliseconds);
@@ -354,8 +362,10 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
else
{
logger.LogInformation(
"SGU profile enrichment completed for role {Role} in {ElapsedMilliseconds} ms.",
BrokerEventIds.ProfileEnrichmentCompleted,
"SGU profile enrichment completed for role {Role} with {ProfileFieldCount} supported fields in {ElapsedMilliseconds} ms.",
identity.Role,
CountProfileFields(profile),
elapsed.ElapsedMilliseconds);
}
@@ -382,6 +392,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
catch (OperationCanceledException)
{
logger.LogWarning(
BrokerEventIds.ProfileEnrichmentTimeout,
"SGU profile request for role {Role} timed out after {ElapsedMilliseconds} ms.",
identity.Role,
elapsed.ElapsedMilliseconds);
@@ -389,6 +400,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
catch (Exception exception)
{
logger.LogWarning(
BrokerEventIds.ProfileEnrichmentFailure,
exception,
"SGU profile enrichment failed for role {Role} after {ElapsedMilliseconds} ms.",
identity.Role,
@@ -423,11 +435,33 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
GetProfileUri(path),
allowedHosts,
timeoutToken).ConfigureAwait(false);
profile = profile.Overlay(html is null ? null : parser(html));
if (html is null)
{
logger.LogWarning(
BrokerEventIds.ProfilePageUnavailable,
"Optional SGU profile page {Path} did not return usable HTML for role {Role}; preserving fields already collected.",
path,
role);
continue;
}
InstitutionalProfile? pageProfile = parser(html);
if (pageProfile is null)
{
logger.LogWarning(
BrokerEventIds.ProfileHtmlUnexpected,
"Optional SGU profile page {Path} returned HTML without its supported field IDs for role {Role}; preserving fields already collected.",
path,
role);
continue;
}
profile = profile.Overlay(pageProfile);
}
catch (OperationCanceledException) when (!requestCancellationToken.IsCancellationRequested)
{
logger.LogWarning(
BrokerEventIds.ProfileEnrichmentTimeout,
"SGU optional staff profile enrichment for role {Role} reached its total timeout after {ElapsedMilliseconds} ms; preserving fields already collected.",
role,
elapsed.ElapsedMilliseconds);
@@ -436,6 +470,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
catch (Exception exception)
{
logger.LogWarning(
BrokerEventIds.ProfileEnrichmentFailure,
exception,
"An optional SGU staff profile page for role {Role} failed after {ElapsedMilliseconds} ms; preserving fields already collected.",
role,
@@ -487,6 +522,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
}
logger.LogWarning(
BrokerEventIds.ProfilePageUnavailable,
"Optional SGU profile page {Path} returned HTTP {StatusCode}.",
requestedUri.AbsolutePath,
statusCode);
@@ -494,6 +530,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
}
logger.LogWarning(
BrokerEventIds.ProfilePageUnavailable,
"Optional SGU profile page {Path} exceeded the redirect limit.",
requestedUri.AbsolutePath);
return null;
@@ -550,6 +587,23 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
right.AbsolutePath.TrimEnd('/'),
StringComparison.OrdinalIgnoreCase);
private static int CountProfileFields(InstitutionalProfile profile) =>
new[]
{
profile.EmployeeNumber,
profile.DisplayName,
profile.GivenName,
profile.Surname,
profile.Email,
profile.EmployeeType,
profile.JobTitle,
profile.Department,
profile.StreetAddress,
profile.City,
profile.State,
profile.PostalCode
}.Count(value => !string.IsNullOrWhiteSpace(value));
private static async Task DrainResponseAsync(
HttpResponseMessage response,
CancellationToken cancellationToken)
@@ -604,17 +658,31 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
response.Content,
options.MaxProfileBytes,
timeoutToken).ConfigureAwait(false);
return identity.Role switch
InstitutionalProfile? profile;
switch (identity.Role)
{
InstitutionalRole.Administrative =>
SguProfileParser.ParseAdministrative(html, identity.NumericId) ??
SguProfileParser.ParseMenu(html),
InstitutionalRole.Student =>
SguProfileParser.ParseStudent(html, identity.NumericId) ??
SguProfileParser.ParseMenu(html),
InstitutionalRole.Professor => SguProfileParser.ParseMenu(html),
_ => null
};
case InstitutionalRole.Administrative:
profile = SguProfileParser.ParseAdministrative(html, identity.NumericId);
break;
case InstitutionalRole.Student:
profile = SguProfileParser.ParseStudent(html, identity.NumericId);
break;
case InstitutionalRole.Professor:
return SguProfileParser.ParseMenu(html);
default:
return null;
}
if (profile is not null)
{
return profile;
}
logger.LogWarning(
BrokerEventIds.ProfileHtmlUnexpected,
"The primary SGU profile HTML did not contain the supported field IDs for role {Role}; attempting the menu-name fallback.",
identity.Role);
return SguProfileParser.ParseMenu(html);
}
private static async Task<string> ReadLimitedStringAsync(