Enrich administrative profiles from SGU
This commit is contained in:
@@ -325,6 +325,21 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
response,
|
||||
identity,
|
||||
timeout.Token).ConfigureAwait(false);
|
||||
if (identity.Role == InstitutionalRole.Administrative &&
|
||||
string.Equals(
|
||||
profile?.EmployeeNumber,
|
||||
identity.NumericId,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
profile = await TryEnrichAdministrativeProfileAsync(
|
||||
client,
|
||||
profile!,
|
||||
allowedHosts,
|
||||
timeout.Token,
|
||||
cancellationToken,
|
||||
elapsed).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (profile is null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
@@ -379,6 +394,104 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<InstitutionalProfile> TryEnrichAdministrativeProfileAsync(
|
||||
HttpClient client,
|
||||
InstitutionalProfile verifiedProfile,
|
||||
HashSet<string> allowedHosts,
|
||||
CancellationToken timeoutToken,
|
||||
CancellationToken requestCancellationToken,
|
||||
Stopwatch elapsed)
|
||||
{
|
||||
InstitutionalProfile profile = verifiedProfile;
|
||||
(string Path, Func<string, InstitutionalProfile?> Parser)[] pages =
|
||||
[
|
||||
(options.AdministrativePersonalProfilePath, SguProfileParser.ParseAdministrativePersonal),
|
||||
(options.AdministrativeLocationProfilePath, SguProfileParser.ParseAdministrativeLocation)
|
||||
];
|
||||
|
||||
foreach ((string path, Func<string, InstitutionalProfile?> parser) in pages)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? html = await TryFetchAdditionalProfilePageAsync(
|
||||
client,
|
||||
GetProfileUri(path),
|
||||
allowedHosts,
|
||||
timeoutToken).ConfigureAwait(false);
|
||||
profile = profile.Overlay(html is null ? null : parser(html));
|
||||
}
|
||||
catch (OperationCanceledException) when (!requestCancellationToken.IsCancellationRequested)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"SGU administrative profile enrichment reached its total timeout after {ElapsedMilliseconds} ms; preserving fields already collected.",
|
||||
elapsed.ElapsedMilliseconds);
|
||||
break;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning(
|
||||
exception,
|
||||
"An optional SGU administrative profile page failed after {ElapsedMilliseconds} ms; preserving fields already collected.",
|
||||
elapsed.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private async Task<string?> TryFetchAdditionalProfilePageAsync(
|
||||
HttpClient client,
|
||||
Uri requestedUri,
|
||||
HashSet<string> allowedHosts,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Uri current = requestedUri;
|
||||
for (int hop = 0; hop <= options.MaxRedirects; hop++)
|
||||
{
|
||||
if (!IsAllowedHttpsUri(current, allowedHosts))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, current);
|
||||
using HttpResponseMessage response = await client
|
||||
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
int statusCode = (int)response.StatusCode;
|
||||
if (statusCode is >= 300 and < 400)
|
||||
{
|
||||
Uri? redirect = ResolveAllowedRedirect(current, response, allowedHosts);
|
||||
if (redirect is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
await DrainResponseAsync(response, cancellationToken).ConfigureAwait(false);
|
||||
current = redirect;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statusCode is >= 200 and < 300)
|
||||
{
|
||||
return await ReadLimitedStringAsync(
|
||||
response.Content,
|
||||
options.MaxProfileBytes,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
logger.LogWarning(
|
||||
"Optional SGU profile page {Path} returned HTTP {StatusCode}.",
|
||||
requestedUri.AbsolutePath,
|
||||
statusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.LogWarning(
|
||||
"Optional SGU profile page {Path} exceeded the redirect limit.",
|
||||
requestedUri.AbsolutePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void AddCredential(
|
||||
Uri uri,
|
||||
CredentialCache credentialCache,
|
||||
@@ -472,6 +585,9 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
return new Uri(endpoint, path);
|
||||
}
|
||||
|
||||
private Uri GetProfileUri(string path) =>
|
||||
new(new Uri(options.Endpoint, UriKind.Absolute), path);
|
||||
|
||||
private async Task<InstitutionalProfile?> TryReadProfileAsync(
|
||||
HttpResponseMessage response,
|
||||
UserIdentity identity,
|
||||
|
||||
Reference in New Issue
Block a user