From 8baa47fe1e2e9ad958b02d40296f6ddda6846b2c Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Tue, 8 Sep 2026 16:12:58 -0600 Subject: [PATCH] Place SGU role groups in their user OUs --- README.md | 10 ++--- docs/architecture.md | 7 +++- scripts/Deploy-AuthBroker.ps1 | 38 +++++++++++++++---- scripts/Publish-GiteaRelease.ps1 | 2 +- src/SGU.AuthBroker/Options/BrokerOptions.cs | 6 +-- src/SGU.AuthBroker/appsettings.json | 6 +-- .../BrokerOptionsTests.cs | 6 +-- 7 files changed, 51 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 65ea37a..ee96ee4 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,11 @@ Operational documentation: - [Domain monitoring, usage reports, and six-month retention](docs/monitoring.md) - [Decision: do not persist password verifiers in Redis](docs/decisions/0001-no-password-cache.md) -| Prefix | Role | Default OU | -|---|---|---| -| `DO` | Professor / docente | `OU=Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx` | -| `AL` | Student / alumno | `OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx` | -| `AD` | Administrative | `OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx` | +| Prefix | Role | Default OU | Security group in the same OU | +|---|---|---|---| +| `DO` | Professor / docente | `OU=Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx` | `SGU-Docentes` | +| `AL` | Student / alumno | `OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx` | `SGU-Alumnos` | +| `AD` | Administrative | `OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx` | `SGU-Administrativos` | If the broker or institutional NTLM authority is unavailable, the provider submits the unchanged credentials to Windows for normal AD/cached-domain diff --git a/docs/architecture.md b/docs/architecture.md index e06deee..753b840 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -76,8 +76,11 @@ password outcome. Every synchronization also enforces one idempotent security-group membership from the classified institutional prefix: `AL` to `SGU-Alumnos`, `AD` to -`SGU-Administrativos`, and `DO` to `SGU-Docentes`. This happens synchronously -inside the broker before the institutional password is written to AD. A missing +`SGU-Administrativos`, and `DO` to `SGU-Docentes`. Each role group is stored +inside its corresponding user OU. During an upgrade, +the bootstrap moves a legacy group from the `Usuarios-SGU` root while preserving +its SID and memberships instead of creating a duplicate. Membership enforcement +happens synchronously inside the broker before the institutional password is written to AD. A missing or inaccessible role group therefore fails provisioning instead of leaving a new usable account without its authorization classification. Existing accounts are repaired automatically on their next successful SGU authentication. diff --git a/scripts/Deploy-AuthBroker.ps1 b/scripts/Deploy-AuthBroker.ps1 index 6e590af..bde2b2d 100644 --- a/scripts/Deploy-AuthBroker.ps1 +++ b/scripts/Deploy-AuthBroker.ps1 @@ -76,16 +76,23 @@ if (-not $serverCertificate.Verify()) { } Import-Module ActiveDirectory -ErrorAction Stop + +function ConvertTo-LdapFilterValue { + param([Parameter(Mandatory)][string]$Value) + + return $Value.Replace('\', '\5c').Replace('*', '\2a').Replace('(', '\28').Replace(')', '\29').Replace(([string][char]0), '\00') +} + $usersOuName = 'Usuarios-SGU' $usersOuDn = "OU=$usersOuName,$BaseDn" if ([string]::IsNullOrWhiteSpace($ProfessorGroupDn)) { - $ProfessorGroupDn = "CN=SGU-Docentes,$usersOuDn" + $ProfessorGroupDn = "CN=SGU-Docentes,OU=Docentes,$usersOuDn" } if ([string]::IsNullOrWhiteSpace($StudentGroupDn)) { - $StudentGroupDn = "CN=SGU-Alumnos,$usersOuDn" + $StudentGroupDn = "CN=SGU-Alumnos,OU=Alumnos,$usersOuDn" } if ([string]::IsNullOrWhiteSpace($AdministrativeGroupDn)) { - $AdministrativeGroupDn = "CN=SGU-Administrativos,$usersOuDn" + $AdministrativeGroupDn = "CN=SGU-Administrativos,OU=Administrativos,$usersOuDn" } if ($CreateMissingOus) { @@ -152,13 +159,30 @@ foreach ($definition in $roleGroupDefinitions) { throw "$($definition.Role)GroupDn must start with a simple CN component." } $groupName = $groupDnMatch.Groups['Name'].Value + $groupPath = $groupDnMatch.Groups['Path'].Value if ($groupName.Length -gt 20) { throw "$($definition.Role) group name exceeds the 20-character sAMAccountName limit." } - New-ADGroup -Name $groupName -SamAccountName $groupName ` - -GroupCategory Security -GroupScope Global ` - -Path $groupDnMatch.Groups['Path'].Value ` - -Description $definition.Description -Server $LdapHost | Out-Null + + $matchingGroups = @(Get-ADGroup ` + -LDAPFilter "(sAMAccountName=$(ConvertTo-LdapFilterValue -Value $groupName))" ` + -SearchBase $BaseDn -SearchScope Subtree -Server $LdapHost -ErrorAction Stop) + if ($matchingGroups.Count -gt 1) { + throw "More than one Active Directory group uses sAMAccountName $groupName; the bootstrap cannot select one safely." + } + if ($matchingGroups.Count -eq 1) { + if ($matchingGroups[0].GroupCategory -ne 'Security') { + throw "$($definition.Role)GroupDn must identify a security group." + } + Move-ADObject -Identity $matchingGroups[0].DistinguishedName ` + -TargetPath $groupPath -Server $LdapHost -Confirm:$false -ErrorAction Stop + } + else { + New-ADGroup -Name $groupName -SamAccountName $groupName ` + -GroupCategory Security -GroupScope Global ` + -Path $groupPath ` + -Description $definition.Description -Server $LdapHost | Out-Null + } $roleGroup = Get-ADGroup -Identity $definition.Dn -Server $LdapHost -ErrorAction Stop } if (-not $roleGroup) { diff --git a/scripts/Publish-GiteaRelease.ps1 b/scripts/Publish-GiteaRelease.ps1 index 4368b9b..36b0224 100644 --- a/scripts/Publish-GiteaRelease.ps1 +++ b/scripts/Publish-GiteaRelease.ps1 @@ -116,7 +116,7 @@ Bootstrap reproducible para el laboratorio SGU. - `sgu-azure-infrastructure-$Version.zip`: despliega mediante Bicep una VM Windows Server 2025, red privada, IP pública protegida por NSG y Azure VPN Gateway P2S; también genera certificados por equipo y descarga el perfil de cliente. - El bootstrap Azure conserva la IP privada administrada por la NIC de Azure, autoriza el pool P2S en los firewalls SGU y nunca publica LDAP, Kerberos, SMB, RPC, WinRM ni el Auth Broker directamente a Internet. - Los Windows 11 Pro pueden instalar un perfil IKEv2 de todos los usuarios con certificado de máquina, DNS dividido para `lci.lasalle.mx` y ejecutarlo desde la pantalla de inicio de sesión antes de autenticar una cuenta de dominio nueva. -- El Auth Broker clasifica sin tareas programadas cada cuenta autenticada: `AL` se agrega a `SGU-Alumnos`, `AD` a `SGU-Administrativos` y `DO` a `SGU-Docentes`; el bootstrap crea estos grupos de seguridad de forma idempotente. +- El Auth Broker clasifica sin tareas programadas cada cuenta autenticada: `AL` se agrega a `SGU-Alumnos`, `AD` a `SGU-Administrativos` y `DO` a `SGU-Docentes`; el bootstrap crea cada grupo dentro de la OU de su rol y migra idempotentemente cualquier grupo heredado sin cambiar su SID. - El enriquecimiento obtiene el sexo de los módulos SGU de personal/alumnos, lo conserva como la línea administrada `SGU-Gender: Male|Female` en Notas de AD y adapta el fondo de Windows/Linux; cuando falta utiliza redacción neutral. - El servidor configura WEF/WEC para registrar sesiones y fallos, inventariar el estado alcanzable de las máquinas cada cinco minutos y conservar durante 183 días tanto esos eventos como el diagnóstico estructurado del Auth Broker. - Windows Home se detecta y se rechaza con una explicación, ya que no admite unión a Active Directory ni RDP host. diff --git a/src/SGU.AuthBroker/Options/BrokerOptions.cs b/src/SGU.AuthBroker/Options/BrokerOptions.cs index 1a2e0c2..84c6322 100644 --- a/src/SGU.AuthBroker/Options/BrokerOptions.cs +++ b/src/SGU.AuthBroker/Options/BrokerOptions.cs @@ -176,11 +176,11 @@ public sealed class ActiveDirectoryOptions 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=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=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=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; diff --git a/src/SGU.AuthBroker/appsettings.json b/src/SGU.AuthBroker/appsettings.json index c9194f9..41551d6 100644 --- a/src/SGU.AuthBroker/appsettings.json +++ b/src/SGU.AuthBroker/appsettings.json @@ -50,9 +50,9 @@ "ProfessorOuDn": "OU=Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", "StudentOuDn": "OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", "AdministrativeOuDn": "OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", - "ProfessorGroupDn": "CN=SGU-Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", - "StudentGroupDn": "CN=SGU-Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", - "AdministrativeGroupDn": "CN=SGU-Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", + "ProfessorGroupDn": "CN=SGU-Docentes,OU=Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", + "StudentGroupDn": "CN=SGU-Alumnos,OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", + "AdministrativeGroupDn": "CN=SGU-Administrativos,OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx", "RemoteDesktopGroupDn": "", "DefaultCompany": "La Salle", "CreateMissingOus": false diff --git a/tests/SGU.AuthBroker.Tests/BrokerOptionsTests.cs b/tests/SGU.AuthBroker.Tests/BrokerOptionsTests.cs index abed443..eb12bb3 100644 --- a/tests/SGU.AuthBroker.Tests/BrokerOptionsTests.cs +++ b/tests/SGU.AuthBroker.Tests/BrokerOptionsTests.cs @@ -31,9 +31,9 @@ public sealed class BrokerOptionsTests } [Theory] - [InlineData(InstitutionalRole.Student, "CN=SGU-Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx")] - [InlineData(InstitutionalRole.Administrative, "CN=SGU-Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx")] - [InlineData(InstitutionalRole.Professor, "CN=SGU-Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx")] + [InlineData(InstitutionalRole.Student, "CN=SGU-Alumnos,OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx")] + [InlineData(InstitutionalRole.Administrative, "CN=SGU-Administrativos,OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx")] + [InlineData(InstitutionalRole.Professor, "CN=SGU-Docentes,OU=Docentes,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx")] public void DefaultRoleGroupMappingsMatchInstitutionalPrefixes(InstitutionalRole role, string expectedGroupDn) { ActiveDirectoryOptions options = new();