using SGU.AuthBroker.Core.Identity; namespace SGU.AuthBroker.Options; public sealed class BrokerOptions { public const string SectionName = "Broker"; public TlsOptions Tls { get; init; } = new(); public NtlmOptions Ntlm { get; init; } = new(); public ActiveDirectoryOptions Directory { get; init; } = new(); public void Validate() { if (Tls.AllowedClientThumbprints.Any(value => !IsCertificateThumbprint(value))) { throw new InvalidOperationException("Every configured client certificate thumbprint must be valid."); } if (!Uri.TryCreate(Ntlm.Endpoint, UriKind.Absolute, out Uri? endpoint) || endpoint.Scheme != Uri.UriSchemeHttps) { throw new InvalidOperationException("The institutional NTLM endpoint must be an absolute HTTPS URL."); } if (Ntlm.AllowedRedirectHosts.Length == 0 || !Ntlm.AllowedRedirectHosts.Contains(endpoint.IdnHost, StringComparer.OrdinalIgnoreCase)) { throw new InvalidOperationException("The NTLM endpoint host must be present in AllowedRedirectHosts."); } if (Ntlm.TimeoutSeconds is < 2 or > 60 || Ntlm.ProfileTimeoutSeconds is < 2 or > 90 || Ntlm.MaxRedirects is < 0 or > 10) { throw new InvalidOperationException("NTLM timeout or redirect limits are outside the supported range."); } if (Ntlm.MaxProfileBytes is < 32 * 1024 or > 2 * 1024 * 1024) { throw new InvalidOperationException("The SGU profile response limit is outside the supported range."); } foreach (string profilePath in new[] { Ntlm.AuthenticationPath, Ntlm.AdministrativeProfilePath, Ntlm.AdministrativePersonalProfilePath, Ntlm.AdministrativeLocationProfilePath, Ntlm.StudentProfilePath, Ntlm.ProfessorPayrollProfilePath, Ntlm.MenuProfilePath }) { if (string.IsNullOrWhiteSpace(profilePath)) { throw new InvalidOperationException("SGU profile paths are required."); } Uri profileUri = new(endpoint, profilePath); if (profileUri.Scheme != Uri.UriSchemeHttps || !string.IsNullOrEmpty(profileUri.UserInfo) || !Ntlm.AllowedRedirectHosts.Contains(profileUri.IdnHost, StringComparer.OrdinalIgnoreCase)) { throw new InvalidOperationException("SGU profile paths must resolve to an allowed HTTPS host."); } } if (string.IsNullOrWhiteSpace(Directory.LdapHost) || string.IsNullOrWhiteSpace(Directory.BaseDn) || string.IsNullOrWhiteSpace(Directory.DomainNetbios) || string.IsNullOrWhiteSpace(Directory.UpnSuffix)) { throw new InvalidOperationException("Active Directory connection and domain settings are required."); } foreach (InstitutionalRole role in Enum.GetValues()) { string ouDn = Directory.GetOuDn(role); if (string.IsNullOrWhiteSpace(ouDn)) { throw new InvalidOperationException($"An OU mapping is required for {role}."); } if (!ouDn.EndsWith($",{Directory.BaseDn}", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"The OU mapping for {role} must be beneath BaseDn."); } string groupDn = Directory.GetGroupDn(role); if (string.IsNullOrWhiteSpace(groupDn) || !groupDn.StartsWith("CN=", StringComparison.OrdinalIgnoreCase) || !groupDn.EndsWith($",{Directory.BaseDn}", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"The security-group mapping for {role} must identify a group beneath BaseDn."); } } if (!string.IsNullOrWhiteSpace(Directory.RemoteDesktopGroupDn) && (!Directory.RemoteDesktopGroupDn.StartsWith("CN=", StringComparison.OrdinalIgnoreCase) || !Directory.RemoteDesktopGroupDn.EndsWith($",{Directory.BaseDn}", StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException("RemoteDesktopGroupDn must identify a group beneath BaseDn."); } if (string.IsNullOrWhiteSpace(Directory.DefaultCompany) || Directory.DefaultCompany.Length > 64) { throw new InvalidOperationException("DefaultCompany is required and must not exceed 64 characters."); } } private static bool IsCertificateThumbprint(string value) { string normalized = value.Replace(" ", string.Empty, StringComparison.Ordinal); return normalized.Length == 40 && normalized.All(Uri.IsHexDigit); } } public sealed class TlsOptions { public string[] AllowedClientThumbprints { get; init; } = []; public bool CheckCertificateRevocation { get; init; } = true; } public sealed class NtlmOptions { public string Endpoint { get; init; } = "https://sgu.ulsa.edu.mx/"; public string Domain { get; init; } = string.Empty; public int TimeoutSeconds { get; init; } = 20; public int ProfileTimeoutSeconds { get; init; } = 90; public int MaxRedirects { get; init; } = 5; public string AuthenticationPath { get; init; } = "/psulsa/"; public string AdministrativeProfilePath { get; init; } = "/psulsa/gadmon/capitalhumano/controlincidencias/incidencias.aspx"; public string AdministrativePersonalProfilePath { get; init; } = "/psulsa/gadmon/capitalhumano/datos/personales.aspx"; public string AdministrativeLocationProfilePath { get; init; } = "/psulsa/gadmon/capitalhumano/datos/ubicacion.aspx"; public string StudentProfilePath { get; init; } = "/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx"; public string ProfessorPayrollProfilePath { get; init; } = "/psulsa/gadmon/nomina/consultanomina.aspx"; public string MenuProfilePath { get; init; } = "/psulsa/menu.aspx"; public int MaxProfileBytes { get; init; } = 512 * 1024; public string[] AllowedRedirectHosts { get; init; } = ["sgu.ulsa.edu.mx"]; } public sealed class ActiveDirectoryOptions { public string LdapHost { get; init; } = "localhost"; public string BaseDn { get; init; } = "DC=lci,DC=lasalle,DC=mx"; public string DomainNetbios { get; init; } = "LCI"; public string UpnSuffix { get; init; } = "lci.lasalle.mx"; public string ProfessorOuDn { get; init; } = "OU=Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx"; public string StudentOuDn { get; init; } = "OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx"; public string AdministrativeOuDn { get; init; } = "OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx"; public string ProfessorGroupDn { get; init; } = "CN=SGU-Docentes,OU=Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx"; public string StudentGroupDn { get; init; } = "CN=SGU-Alumnos,OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx"; public string AdministrativeGroupDn { get; init; } = "CN=SGU-Administrativos,OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx"; public string RemoteDesktopGroupDn { get; init; } = string.Empty; public string DefaultCompany { get; init; } = "La Salle"; public bool CreateMissingOus { get; init; } public string GetOuDn(InstitutionalRole role) => role switch { InstitutionalRole.Professor => ProfessorOuDn, InstitutionalRole.Student => StudentOuDn, InstitutionalRole.Administrative => AdministrativeOuDn, _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) }; public string GetGroupDn(InstitutionalRole role) => role switch { InstitutionalRole.Professor => ProfessorGroupDn, InstitutionalRole.Student => StudentGroupDn, InstitutionalRole.Administrative => AdministrativeGroupDn, _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) }; }