Files
Disco/Disco.Services/Interop/ActiveDirectory/ADDeviceOfflineDomainJoining.cs
T
Gary Sharp 48512fa9d1 qol: offline domain join to reuse AD computer accounts
Replaces old behaviour of deleting and creating new accounts. Now when a device has a new name, its existing account is renamed and reused.
2026-02-25 14:34:34 +11:00

101 lines
5.2 KiB
C#

using System;
using System.Linq;
using System.Runtime.InteropServices;
namespace Disco.Services.Interop.ActiveDirectory
{
public static class ADDeviceOfflineDomainJoining
{
[Flags]
private enum NETSETUP_PROVISION_FLAGS : int
{
NETSETUP_PROVISION_DOWNLEVEL_PRIV_SUPPORT = 0x00000001,
NETSETUP_PROVISION_REUSE_ACCOUNT = 0x00000002,
NETSETUP_PROVISION_USE_DEFAULT_PASSWORD = 0x00000004,
NETSETUP_PROVISION_SKIP_ACCOUNT_SEARCH = 0x00000008,
NETSETUP_PROVISION_ROOT_CA_CERTS = 0x00000010,
NETSETUP_PROVISION_PERSISTENTSITE = 0x00000020,
NETSETUP_PROVISION_ONLINE_CALLER = 0x40000000,
NETSETUP_PROVISION_CHECK_PWD_ONLY = unchecked((int)0x80000000),
}
[DllImport("Netapi32.dll", CallingConvention = CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.I4)]
private static extern int NetProvisionComputerAccount(
[In, MarshalAs(UnmanagedType.LPWStr)] string lpDomain,
[In, MarshalAs(UnmanagedType.LPWStr)] string lpMachineName,
[In, Optional, MarshalAs(UnmanagedType.LPWStr)] string lpMachineAccountOU,
[In, Optional, MarshalAs(UnmanagedType.LPWStr)] string lpDcName,
[In, MarshalAs(UnmanagedType.I4)] NETSETUP_PROVISION_FLAGS dwOptions,
out IntPtr pProvisionBinData,
out int pdwProvisionBinDataSize,
[Optional] IntPtr pProvisionTextData
);
[DllImport("Netapi32.dll", SetLastError = true)]
private static extern int NetApiBufferFree(IntPtr Buffer);
public static string OfflineDomainJoinProvision(this ADDomainController dc, string computerSamAccountName, string organisationalUnit, ref ADMachineAccount machineAccount)
{
if (machineAccount != null && machineAccount.IsCriticalSystemObject)
throw new InvalidOperationException($"This account {machineAccount.DistinguishedName} is a Critical System Active Directory Object and Disco ICT refuses to modify it");
if (!dc.IsWritable)
throw new InvalidOperationException($"The domain controller [{dc.Name}] is not writable. This action (Offline Domain Join Provision) requires a writable domain controller.");
if (!string.IsNullOrWhiteSpace(computerSamAccountName))
computerSamAccountName = computerSamAccountName.TrimEnd('$');
if (!string.IsNullOrWhiteSpace(computerSamAccountName) && computerSamAccountName.Contains('\\'))
computerSamAccountName = computerSamAccountName.Substring(computerSamAccountName.IndexOf('\\') + 1);
// NetBIOS Limit (16 characters; "{ComputerName}$"; 15 characters allowed)
if (string.IsNullOrWhiteSpace(computerSamAccountName) || computerSamAccountName.Length > 15)
throw new ArgumentException("Invalid Computer Name; > 0 and <= 15", "ComputerName");
// Ensure Specified OU Exists
if (!string.IsNullOrEmpty(organisationalUnit))
{
try
{
using (var deOU = dc.RetrieveDirectoryEntry(organisationalUnit, new string[] { "distinguishedName" }))
{
if (deOU == null)
throw new Exception($"OU's Directory Entry couldn't be found at [{organisationalUnit}]");
}
}
catch (Exception ex)
{
throw new ArgumentException($"An error occurred while trying to locate the specified OU: {organisationalUnit}", "OrganisationalUnit", ex);
}
}
if (machineAccount != null && !string.Equals(machineAccount.Name, computerSamAccountName, StringComparison.Ordinal))
{
// rename the account
machineAccount.RenameAccount(dc, computerSamAccountName);
}
var result = NetProvisionComputerAccount(dc.Domain.Name, computerSamAccountName, string.IsNullOrWhiteSpace(organisationalUnit) ? null : organisationalUnit, dc.Name, NETSETUP_PROVISION_FLAGS.NETSETUP_PROVISION_REUSE_ACCOUNT, out var provisionDataPointer, out var provisionDataLength);
if (result != 0)
{
var win32Exception = new System.ComponentModel.Win32Exception(result);
throw new InvalidOperationException($"NetProvisionComputerAccount failed with error code {result}: {win32Exception.Message}");
}
if (provisionDataPointer == IntPtr.Zero || provisionDataLength == 0)
throw new InvalidOperationException("NetProvisionComputerAccount did not return valid provisioning data.");
var buffer = new byte[provisionDataLength];
Marshal.Copy(provisionDataPointer, buffer, 0, provisionDataLength);
var freeResult = NetApiBufferFree(provisionDataPointer);
var encodedResult = Convert.ToBase64String(buffer);
// Reload Machine Account
machineAccount = dc.RetrieveADMachineAccount($@"{dc.Domain.NetBiosName}\{computerSamAccountName}", (machineAccount == null ? null : machineAccount.LoadedProperties.Keys.ToArray()));
return encodedResult;
}
}
}