Problema

En entornos con cientos de unidades organizativas (OUs) es frecuente aplicar la opción “Protect object from accidental deletion” a OUs críticos. Esa opción inserta ACEs de Delete y DeleteTree con denegación en el descriptor de seguridad del OU. Además, muchas organizaciones añaden un ACE DeleteChild Deny a la OU padre para impedir que cualquier usuario elimine accidentalmente cualquiera de sus hijos.

El problema surge cuando una herramienta de automatización necesita eliminar una OU específica que está protegida, pero el padre sigue teniendo el ACE DeleteChild Deny para Everyone. En la consola ADUC, desmarcar la protección del OU hijo permite la eliminación sin tocar el ACE del padre. En cambio, al usar LDAP (por ejemplo, con ldap3 en Python) o PowerShell, la lógica de autorización parece bloquear la operación antes de que el servidor AD evalúe la excepción del ACE del hijo.

El reto es reproducir el comportamiento de ADUC: eliminar solo el OU objetivo sin debilitar la protección de los OUs hermanos ni modificar permanentemente la ACL del padre.

Causa

Active Directory evalúa los permisos de forma jerárquica pero con reglas bien definidas:

  1. Permiso DELETE en el objeto objetivo – Necesario para cualquier borrado. Un ACE de denegación en el propio objeto (Delete/DeleteTree) anula cualquier permiso concedido en niveles superiores.
  2. Permiso DELETE_CHILD en el contenedor – Necesario cuando el borrado se realiza a través del contenedor (por ejemplo, ldap delete sin especificar el DN completo). Si el contenedor tiene un ACE Deny DeleteChild para el usuario, la operación se rechaza antes de inspeccionar el descriptor del hijo.
  3. ADUC primero elimina la protección del OU hijo (quita los ACE de denegación en el propio objeto) y luego llama a la API de borrado que usa la ruta completa del objeto. La llamada incluye el DN del OU a borrar, por lo que el servidor solo necesita validar el permiso DELETE en el objeto; el ACE DeleteChild del padre no entra en juego.
  4. Bibliotecas LDAP a menudo envían la petición como “delete entry” sin especificar la ruta completa o usan el control de borrado de árbol, lo que fuerza al servidor a comprobar DeleteChild en el contenedor. Si el ACE de denegación está presente, la petición falla antes de que se inspeccione la protección del hijo.

En resumen, la discrepancia proviene de cómo se construye la petición LDAP y de qué permisos se evalúan primero.

Solución

La solución genérica consiste en evitar que la evaluación llegue al ACE DeleteChild del padre. Se logra siguiendo estos pasos, aplicables tanto con PowerShell, .NET como con cualquier cliente LDAP:

  1. Desactivar temporalmente la protección del OU objetivo.
    • Quitar los ACE de denegación Delete y DeleteTree del descriptor del OU.
    • No tocar la ACL del contenedor padre.
  2. Ejecutar la eliminación usando el DN completo del OU.
    • En PowerShell: Remove-ADObject -Identity $dn -Recursive:$false.
    • En LDAP: enviar una petición DeleteRequest con el DN exacto y sin el control de borrado de árbol.
  3. Restaurar la protección del OU (opcional).
    • Si el OU se elimina, no hay nada que restaurar; si la operación falla, volver a aplicar los ACE originales.

Este flujo reproduce exactamente lo que hace ADUC y mantiene intactos los ACE de denegación en el contenedor, por lo que los OUs hermanos siguen protegidos.

Alternativas prácticas

Enfoque Ventajas Desventajas
Modificar solo la security descriptor del OU (Set‑ADOrganizationalUnit –ProtectedFromAccidentalDeletion $false) Simple, usa cmdlet nativo, no afecta al padre Requiere dos llamadas (desactivar → borrar → opcionalmente volver a activar)
Eliminar el ACE DeleteChild del padre Evita la necesidad de tocar el OU hijo Afecta a todos los hijos; riesgo de borrado accidental de OUs no relacionados
Usar el control LDAP “Tree Delete” Permite borrar recursivamente sin tocar ACLs El control obliga a que el servidor evalúe DeleteChild; falla si el ACE está denegado

En la práctica, la primera opción (modificar solo el OU objetivo) es la más segura y la que ADUC implementa internamente.

Cuándo aplicar esta solución

  • Entornos con protección granular: cuando varios OUs hijos están marcados como “protected” y el contenedor padre tiene un ACE DeleteChild Deny para evitar borrados masivos.
  • Automatizaciones que gestionan la vida de OUs temporales: scripts que crean OUs para pruebas y deben eliminarlas sin interferir con la política de seguridad.
  • Cuentas de servicio con privilegios limitados: cuando la cuenta tiene permiso DELETE en el OU pero no DeleteChild en el padre.
  • No aplicar si la política de seguridad exige que cualquier borrado pase por una revisión manual del ACE del contenedor, o si la cuenta de servicio ya posee permiso DeleteChild explícito.

Código

# PowerShell: desactivar protección, borrar OU y (opcional) volver a activar
$ouDn = "OU=Disabled,OU=Copilot-MCP-Test,DC=mcpdemo,DC=local"

# 1. Desactivar protección del OU
Set-ADOrganizationalUnit -Identity $ouDn -ProtectedFromAccidentalDeletion $false

# 2. Borrar el OU usando su DN completo
Remove-ADObject -Identity $ouDn -Confirm:$false

# 3. (Opcional) Si el borrado falla y quieres restaurar la protección
# Set-ADOrganizationalUnit -Identity $ouDn -ProtectedFromAccidentalDeletion $true

Para entornos que usan ldap3 en Python, la lógica equivalente sería:

from ldap3 import Server, Connection, MODIFY_REPLACE, ALL

server = Server('ldaps://dc01.mcpdemo.local', get_info=ALL)
conn = Connection(server, user='DOMAIN\\svc_account', password='Secret', auto_bind=True)

ou_dn = 'OU=Disabled,OU=Copilot-MCP-Test,DC=mcpdemo,DC=local'

# 1. Quitar ACE Delete/DeleteTree (simplificado: set nTSecurityDescriptor = null)
# En producción se debería leer el descriptor, eliminar los ACE y volver a escribirlo.
# Aquí se muestra la llamada básica:
conn.modify(ou_dn, {'nTSecurityDescriptor': [(MODIFY_REPLACE, [b''] )]})

# 2. Borrar el OU con su DN completo
conn.delete(ou_dn)

# 3. Cerrar conexión
conn.unbind()

Nota: el ejemplo Python omite la manipulación detallada del descriptor por claridad; en producción se recomienda usar pyad o ldap3 con SecurityDescriptor para modificar ACEs de forma segura.


## Verificación
1. **Antes de ejecutar**: listar los ACE del OU hijo y del contenedor padre con `Get-Acl` o `dsacls`. Verificar que el padre contiene `DeleteChild` Deny para *Everyone* y que el hijo tiene `Delete` Deny.
2. **Ejecutar el script**: observar que la eliminación ocurre sin errores de “Insufficient access rights”.
3. **Después**: volver a listar los ACE del contenedor padre. Deben ser idénticos a los originales, confirmando que no se alteró la protección de los OUs hermanos.
4. **Prueba de regresión**: intentar borrar otro OU hermano sin desactivar su protección. La operación debe fallar, demostrando que la política del padre sigue vigente.

## Notas adicionales
- **Cache de permisos**: después de modificar el descriptor, puede ser necesario forzar una actualización de la caché de AD (por ejemplo, con `repadmin /syncall`) si la eliminación ocurre inmediatamente en otro controlador.
- **Auditoría**: habilitar auditoría de cambios en objetos de tipo `organizationalUnit` ayuda a rastrear quién desactiva la protección y cuándo.
- **Principio de menor privilegio**: si la cuenta de servicio puede recibir el permiso `DeleteChild` explícito en el contenedor, la solución de desactivar la protección del hijo ya no es necesaria. Evalúa agregar un ACE `Allow DeleteChild` solo a la cuenta de servicio.
- **Entornos multiregión**: en bosques con controladores de dominio en diferentes sitios, asegúrate de que la replicación haya concluido antes de intentar la eliminación, para evitar “Object not found” tras la eliminación parcial.