Adapt welcome wallpaper to SGU gender
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace SGU.AuthBroker.Core.Profiles;
|
||||
|
||||
public enum InstitutionalGender
|
||||
{
|
||||
Male,
|
||||
Female
|
||||
}
|
||||
@@ -12,7 +12,8 @@ public sealed record InstitutionalProfile(
|
||||
string? StreetAddress = null,
|
||||
string? City = null,
|
||||
string? State = null,
|
||||
string? PostalCode = null)
|
||||
string? PostalCode = null,
|
||||
InstitutionalGender? Gender = null)
|
||||
{
|
||||
public bool HasValues =>
|
||||
EmployeeNumber is not null ||
|
||||
@@ -26,7 +27,8 @@ public sealed record InstitutionalProfile(
|
||||
StreetAddress is not null ||
|
||||
City is not null ||
|
||||
State is not null ||
|
||||
PostalCode is not null;
|
||||
PostalCode is not null ||
|
||||
Gender is not null;
|
||||
|
||||
public InstitutionalProfile Overlay(InstitutionalProfile? values) =>
|
||||
values is null
|
||||
@@ -44,6 +46,7 @@ public sealed record InstitutionalProfile(
|
||||
StreetAddress = values.StreetAddress ?? StreetAddress,
|
||||
City = values.City ?? City,
|
||||
State = values.State ?? State,
|
||||
PostalCode = values.PostalCode ?? PostalCode
|
||||
PostalCode = values.PostalCode ?? PostalCode,
|
||||
Gender = values.Gender ?? Gender
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ public static class SguProfileParser
|
||||
private const string AdministrativeGivenNameId = "ctl00_contenedor_txtNombre";
|
||||
private const string AdministrativePaternalSurnameId = "ctl00_contenedor_txtApaterno";
|
||||
private const string AdministrativeMaternalSurnameId = "ctl00_contenedor_txtAmaterno";
|
||||
private const string AdministrativeGenderId = "ctl00_contenedor_ddlsexo";
|
||||
private const string AdministrativeGenderName = "ctl00$contenedor$ddlsexo";
|
||||
private const string AdministrativeStreetId = "ctl00_contenedor_txtCalle";
|
||||
private const string AdministrativeExteriorNumberId = "ctl00_contenedor_txtNoExt";
|
||||
private const string AdministrativeInteriorNumberId = "ctl00_contenedor_txtNoInt";
|
||||
@@ -34,6 +36,7 @@ public static class SguProfileParser
|
||||
private const string StudentCityId = "ctl00_contenedor_HistorialAlumno1_lblCiudadAlumnoHP";
|
||||
private const string StudentMunicipalityId = "ctl00_contenedor_HistorialAlumno1_lblDeloMunAlumnoHP";
|
||||
private const string StudentPostalCodeId = "ctl00_contenedor_HistorialAlumno1_lblCPAlumnoHP";
|
||||
private const string StudentGenderId = "ctl00_contenedor_HistorialAlumno1_lblSexoAlumnoHP";
|
||||
|
||||
public static InstitutionalProfile? ParseAdministrative(string html, string expectedEmployeeNumber)
|
||||
=> ParseStaffHeader(html, expectedEmployeeNumber);
|
||||
@@ -91,7 +94,11 @@ public static class SguProfileParser
|
||||
InstitutionalProfile profile = new(
|
||||
DisplayName: displayName,
|
||||
GivenName: givenName,
|
||||
Surname: surname);
|
||||
Surname: surname,
|
||||
Gender: ParseStaffGender(ExtractSelectedOptionValue(
|
||||
html,
|
||||
AdministrativeGenderId,
|
||||
AdministrativeGenderName)));
|
||||
return profile.HasValues ? profile : null;
|
||||
}
|
||||
|
||||
@@ -151,7 +158,8 @@ public static class SguProfileParser
|
||||
StreetAddress: streetAddress,
|
||||
City: city ?? municipality,
|
||||
State: NormalizeTitle(ExtractSpanText(html, StudentStateId), 128),
|
||||
PostalCode: NormalizePostalCode(ExtractSpanText(html, StudentPostalCodeId)));
|
||||
PostalCode: NormalizePostalCode(ExtractSpanText(html, StudentPostalCodeId)),
|
||||
Gender: ParseStudentGender(ExtractSpanText(html, StudentGenderId)));
|
||||
return profile.HasValues ? profile : null;
|
||||
}
|
||||
|
||||
@@ -292,11 +300,68 @@ public static class SguProfileParser
|
||||
return nonPlaceholderOptions.Count == 1 ? nonPlaceholderOptions[0] : null;
|
||||
}
|
||||
|
||||
private static string? ExtractSelectedOptionValue(string html, string id, string name)
|
||||
{
|
||||
string? openingTag = FindOpeningTag(html, "select", id) ??
|
||||
FindOpeningTagByAttribute(html, "select", "name", name);
|
||||
if (openingTag is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int openingTagIndex = html.IndexOf(openingTag, StringComparison.OrdinalIgnoreCase);
|
||||
int contentStart = openingTagIndex + openingTag.Length;
|
||||
int contentEnd = html.IndexOf("</select", contentStart, StringComparison.OrdinalIgnoreCase);
|
||||
if (openingTagIndex < 0 || contentEnd < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string? selectedValue = ExtractAttributeValue(openingTag, "value");
|
||||
string optionsHtml = html[contentStart..contentEnd];
|
||||
int searchFrom = 0;
|
||||
while (searchFrom < optionsHtml.Length)
|
||||
{
|
||||
int optionStart = optionsHtml.IndexOf("<option", searchFrom, StringComparison.OrdinalIgnoreCase);
|
||||
if (optionStart < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int optionTagEnd = optionsHtml.IndexOf('>', optionStart);
|
||||
if (optionTagEnd < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
string optionTag = optionsHtml[optionStart..(optionTagEnd + 1)];
|
||||
string? optionValue = ExtractAttributeValue(optionTag, "value");
|
||||
if (optionValue is not null &&
|
||||
(HasAttribute(optionTag, "selected") ||
|
||||
(selectedValue is not null &&
|
||||
string.Equals(optionValue, selectedValue, StringComparison.Ordinal))))
|
||||
{
|
||||
return NormalizeText(optionValue);
|
||||
}
|
||||
|
||||
searchFrom = optionTagEnd + 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? FindOpeningTag(string html, string tagName, string id)
|
||||
=> FindOpeningTagByAttribute(html, tagName, "id", id);
|
||||
|
||||
private static string? FindOpeningTagByAttribute(
|
||||
string html,
|
||||
string tagName,
|
||||
string attributeName,
|
||||
string attributeValue)
|
||||
{
|
||||
foreach (char quote in new[] { '"', '\'' })
|
||||
{
|
||||
string marker = $"id={quote}{id}{quote}";
|
||||
string marker = $"{attributeName}={quote}{attributeValue}{quote}";
|
||||
int searchFrom = 0;
|
||||
while (searchFrom < html.Length)
|
||||
{
|
||||
@@ -324,6 +389,20 @@ public static class SguProfileParser
|
||||
return null;
|
||||
}
|
||||
|
||||
private static InstitutionalGender? ParseStaffGender(string? value) => value?.Trim() switch
|
||||
{
|
||||
"1" => InstitutionalGender.Male,
|
||||
"2" => InstitutionalGender.Female,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static InstitutionalGender? ParseStudentGender(string? value) => value?.Trim().ToUpperInvariant() switch
|
||||
{
|
||||
"M" => InstitutionalGender.Male,
|
||||
"F" => InstitutionalGender.Female,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static string? ExtractAttributeValue(string openingTag, string attributeName)
|
||||
{
|
||||
foreach (char quote in new[] { '"', '\'' })
|
||||
|
||||
@@ -11,6 +11,8 @@ public sealed class ActiveDirectorySynchronizer(
|
||||
BrokerOptions options,
|
||||
ILogger<ActiveDirectorySynchronizer> logger) : IActiveDirectorySynchronizer
|
||||
{
|
||||
private const string GenderMetadataPrefix = "SGU-Gender:";
|
||||
private const int InfoAttributeMaximumLength = 1024;
|
||||
private const int AccountDisabled = 0x0002;
|
||||
private const int NormalAccount = 0x0200;
|
||||
private static readonly AuthenticationTypes BindFlags =
|
||||
@@ -164,6 +166,7 @@ public sealed class ActiveDirectorySynchronizer(
|
||||
SetOptionalProperty(user, "l", profile.City);
|
||||
SetOptionalProperty(user, "st", profile.State);
|
||||
SetOptionalProperty(user, "postalCode", profile.PostalCode);
|
||||
SetGenderMetadata(user, profile.Gender, identity.UserName, logger);
|
||||
if (string.Equals(profile.EmployeeNumber, identity.NumericId, StringComparison.Ordinal))
|
||||
{
|
||||
SetOptionalProperty(user, "employeeID", profile.EmployeeNumber);
|
||||
@@ -201,6 +204,56 @@ public sealed class ActiveDirectorySynchronizer(
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetGenderMetadata(
|
||||
DirectoryEntry entry,
|
||||
InstitutionalGender? gender,
|
||||
string institutionalUser,
|
||||
ILogger logger)
|
||||
{
|
||||
if (gender is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string existing = Convert.ToString(entry.Properties["info"].Value) ?? string.Empty;
|
||||
string? updated = MergeGenderMetadata(existing, gender);
|
||||
if (updated is null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
BrokerEventIds.DirectoryOptionalMetadataFailure,
|
||||
"Gender metadata was not written for {InstitutionalUser} because the Active Directory info attribute has no remaining capacity.",
|
||||
institutionalUser);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.Properties["info"].Value = updated;
|
||||
}
|
||||
|
||||
internal static string? MergeGenderMetadata(
|
||||
string? existing,
|
||||
InstitutionalGender? gender)
|
||||
{
|
||||
if (gender is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string managedLine = $"{GenderMetadataPrefix} {gender}";
|
||||
string normalizedExisting = (existing ?? string.Empty)
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Replace('\r', '\n');
|
||||
string[] preservedLines = string.IsNullOrEmpty(normalizedExisting)
|
||||
? []
|
||||
: normalizedExisting
|
||||
.Split('\n')
|
||||
.Where(line => !line.TrimStart().StartsWith(
|
||||
GenderMetadataPrefix,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
string updated = string.Join("\r\n", preservedLines.Append(managedLine));
|
||||
return updated.Length <= InfoAttributeMaximumLength ? updated : null;
|
||||
}
|
||||
|
||||
private void EnsureRoleGroupMembership(DirectoryEntry user, UserIdentity identity)
|
||||
{
|
||||
user.RefreshCache(["distinguishedName"]);
|
||||
|
||||
Reference in New Issue
Block a user