diff --git a/Disco.Client/Extensions/EnrolExtensions.cs b/Disco.Client/Extensions/EnrolExtensions.cs
index 64348bb3..129a5086 100644
--- a/Disco.Client/Extensions/EnrolExtensions.cs
+++ b/Disco.Client/Extensions/EnrolExtensions.cs
@@ -2,8 +2,7 @@
using Disco.Models.ClientServices;
using Microsoft.Win32;
using System;
-using System.Diagnostics;
-using System.IO;
+using System.Runtime.InteropServices;
namespace Disco.Client.Extensions
{
@@ -61,6 +60,28 @@ namespace Disco.Client.Extensions
Program.AllowUninstall = enrolResponse.AllowBootstrapperUninstall;
}
+ [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 NetRequestOfflineDomainJoin(
+ [In] IntPtr pProvisionBinData,
+ [In, MarshalAs(UnmanagedType.I4)] int cbProvisionBinDataSize,
+ [In, MarshalAs(UnmanagedType.I4)] NETSETUP_PROVISION_FLAGS dwOptions,
+ [In, MarshalAs(UnmanagedType.LPWStr)] string lpWindowsPath
+ );
+
///
/// Processes a Client Service Enrol Response for Offline Domain Join Actions
///
@@ -72,30 +93,31 @@ namespace Disco.Client.Extensions
{
Presentation.UpdateStatus("Enrolling Device", $"Performing Offline Domain Join:\r\nRenaming Computer: {Environment.MachineName} -> {enrolResponse.ComputerName}", true, -1, 1500);
- string odjFile = Path.GetTempFileName();
- File.WriteAllBytes(odjFile, Convert.FromBase64String(enrolResponse.OfflineDomainJoinManifest));
+ var provisionData = Convert.FromBase64String(enrolResponse.OfflineDomainJoinManifest);
+ string systemRoot = Environment.GetEnvironmentVariable("SystemRoot");
- string odjWindowsPath = Environment.GetEnvironmentVariable("SystemRoot");
- string odjProcessArguments = $"/REQUESTODJ /LOADFILE \"{odjFile}\" /WINDOWSPATH \"{odjWindowsPath}\" /LOCALOS";
-
- ProcessStartInfo odjProcessStartInfo = new ProcessStartInfo("DJOIN.EXE", odjProcessArguments)
+ var provisionDataPointer = Marshal.AllocCoTaskMem(provisionData.Length);
+ Marshal.Copy(provisionData, 0, provisionDataPointer, provisionData.Length);
+ var joinResult = default(int);
+ try
{
- CreateNoWindow = true,
- ErrorDialog = false,
- LoadUserProfile = true,
- RedirectStandardOutput = true,
- UseShellExecute = false
- };
- string odjResult;
- using (Process odjProcess = System.Diagnostics.Process.Start(odjProcessStartInfo))
- {
- odjResult = odjProcess.StandardOutput.ReadToEnd();
- odjProcess.WaitForExit(20000); // 20 Seconds
+ joinResult = NetRequestOfflineDomainJoin(provisionDataPointer, provisionData.Length, NETSETUP_PROVISION_FLAGS.NETSETUP_PROVISION_ONLINE_CALLER, systemRoot);
+ }
+ finally
+ {
+ Marshal.FreeCoTaskMem(provisionDataPointer);
}
- Presentation.UpdateStatus("Enrolling Device", $"Offline Domain Join Result:\r\n{odjResult}", true, -1, 3000);
- if (File.Exists(odjFile))
- File.Delete(odjFile);
+ if (joinResult != 0)
+ {
+ var win32Exception = new System.ComponentModel.Win32Exception(joinResult);
+ Presentation.UpdateStatus("Enrolling Device", $"Offline Domain Join Failed:\r\n{win32Exception.Message} [{joinResult}]", true, -1, 3000);
+ throw new InvalidOperationException($"Offline Domain Join Failed:\r\n{win32Exception.Message} [{joinResult}]");
+ }
+ else
+ {
+ Presentation.UpdateStatus("Enrolling Device", $"Offline Domain Join Succeeded", true, -1, 2000);
+ }
// Flush Logged-On History
if (enrolResponse.SetAssignedUserForLogon && !string.IsNullOrEmpty(enrolResponse.DomainName))
diff --git a/Disco.Services/Devices/Enrolment/WindowsDeviceEnrolment.cs b/Disco.Services/Devices/Enrolment/WindowsDeviceEnrolment.cs
index ecf5f2bd..7cc95e9b 100644
--- a/Disco.Services/Devices/Enrolment/WindowsDeviceEnrolment.cs
+++ b/Disco.Services/Devices/Enrolment/WindowsDeviceEnrolment.cs
@@ -188,9 +188,11 @@ namespace Disco.Services.Devices.Enrolment
{
if (!authenticatedToken.Has(Claims.ComputerAccount))
throw new EnrolmentSafeException($"Connection not correctly authenticated (SN: {Request.SerialNumber}; Auth User: {authenticatedToken.User.UserId})");
+ else if (!string.Equals($"{Request.ComputerName}$", authenticatedToken.User.UserId, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException($"Connection not correctly authenticated (SN: {Request.SerialNumber}; Computer Name: {Request.ComputerName}; Auth User: {authenticatedToken.User.UserId})");
- if (domain == null)
- domain = ActiveDirectory.Context.GetDomainByName(Request.DNSDomainName);
+ if (domain == null && !ActiveDirectory.Context.TryGetDomainByName(Request.DNSDomainName, out domain))
+ throw new EnrolmentSafeException($"The specified domain name '{Request.DNSDomainName}' is not recognized or reachable.");
if (!authenticatedToken.User.UserId.Equals($@"{domain.NetBiosName}\{Request.ComputerName}$", StringComparison.OrdinalIgnoreCase))
throw new EnrolmentSafeException($"Connection not correctly authenticated (SN: {Request.SerialNumber}; Auth User: {authenticatedToken.User.UserId})");
@@ -392,9 +394,7 @@ namespace Disco.Services.Devices.Enrolment
EnrolmentLog.LogSessionTaskProvisioningADAccount(sessionId, device.SerialNumber, device.DeviceDomainId);
adMachineAccount = domainController.Value.RetrieveADMachineAccount(device.DeviceDomainId);
- response.OfflineDomainJoinManifest = domainController.Value.OfflineDomainJoinProvision(device.DeviceDomainId, device.DeviceProfile.OrganisationalUnit, ref adMachineAccount, out var offlineProvisionDiagnosicInfo);
-
- EnrolmentLog.LogSessionDiagnosticInformation(sessionId, offlineProvisionDiagnosicInfo);
+ response.OfflineDomainJoinManifest = domainController.Value.OfflineDomainJoinProvision(device.DeviceDomainId, device.DeviceProfile.OrganisationalUnit, ref adMachineAccount);
response.RequireReboot = true;
}
@@ -441,10 +441,7 @@ namespace Disco.Services.Devices.Enrolment
response.ComputerName = calculatedAccountUsername;
// Create New Account
-
- response.OfflineDomainJoinManifest = domainController.Value.OfflineDomainJoinProvision(device.DeviceDomainId, device.DeviceProfile.OrganisationalUnit, ref adMachineAccount, out var offlineProvisionDiagnosicInfo);
-
- EnrolmentLog.LogSessionDiagnosticInformation(sessionId, offlineProvisionDiagnosicInfo);
+ response.OfflineDomainJoinManifest = domainController.Value.OfflineDomainJoinProvision(device.DeviceDomainId, device.DeviceProfile.OrganisationalUnit, ref adMachineAccount);
response.RequireReboot = true;
}
diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj
index fca0d1c5..8e5ce064 100644
--- a/Disco.Services/Disco.Services.csproj
+++ b/Disco.Services/Disco.Services.csproj
@@ -464,6 +464,7 @@
+
diff --git a/Disco.Services/Interop/ActiveDirectory/ADDeviceOfflineDomainJoining.cs b/Disco.Services/Interop/ActiveDirectory/ADDeviceOfflineDomainJoining.cs
new file mode 100644
index 00000000..b304a922
--- /dev/null
+++ b/Disco.Services/Interop/ActiveDirectory/ADDeviceOfflineDomainJoining.cs
@@ -0,0 +1,100 @@
+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;
+ }
+ }
+}
diff --git a/Disco.Services/Interop/ActiveDirectory/ADDomainController.cs b/Disco.Services/Interop/ActiveDirectory/ADDomainController.cs
index bd0a2662..15182304 100644
--- a/Disco.Services/Interop/ActiveDirectory/ADDomainController.cs
+++ b/Disco.Services/Interop/ActiveDirectory/ADDomainController.cs
@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Linq;
using System.Net.NetworkInformation;
using System.Security.Principal;
-using System.Text;
namespace Disco.Services.Interop.ActiveDirectory
{
@@ -353,93 +351,6 @@ namespace Disco.Services.Interop.ActiveDirectory
return (pr.Status == IPStatus.Success);
}
}
-
- public string OfflineDomainJoinProvision(string ComputerSamAccountName, string OrganisationalUnit, ref ADMachineAccount MachineAccount, out string DiagnosticInformation)
- {
- 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 (!IsWritable)
- throw new InvalidOperationException($"The domain controller [{Name}] is not writable. This action (Offline Domain Join Provision) requires a writable domain controller.");
-
- StringBuilder diagnosticInfo = new StringBuilder();
- string DJoinResult = null;
-
- 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 = 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)
- MachineAccount.DeleteAccount(this);
-
- string tempFileName = System.IO.Path.GetTempFileName();
- string argumentOU = (!string.IsNullOrWhiteSpace(OrganisationalUnit)) ? $" /MACHINEOU \"{OrganisationalUnit}\"" : string.Empty;
- string arguments = $"/PROVISION /DOMAIN \"{Domain.Name}\" /DCNAME \"{Name}\" /MACHINE \"{ComputerSamAccountName}\"{argumentOU} /REUSE /SAVEFILE \"{tempFileName}\"";
- ProcessStartInfo commandStarter = new ProcessStartInfo("DJOIN.EXE", arguments)
- {
- CreateNoWindow = true,
- ErrorDialog = false,
- LoadUserProfile = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false
- };
- diagnosticInfo.AppendFormat($"DJOIN.EXE {arguments}");
- diagnosticInfo.AppendLine();
-
- string stdOutput;
- string stdError;
- using (Process commandProc = Process.Start(commandStarter))
- {
- commandProc.WaitForExit(20000);
- stdOutput = commandProc.StandardOutput.ReadToEnd();
- stdError = commandProc.StandardError.ReadToEnd();
- }
- if (!string.IsNullOrWhiteSpace(stdOutput))
- diagnosticInfo.AppendLine(stdOutput);
- if (!string.IsNullOrWhiteSpace(stdError))
- diagnosticInfo.AppendLine(stdError);
-
- if (System.IO.File.Exists(tempFileName))
- {
- DJoinResult = Convert.ToBase64String(System.IO.File.ReadAllBytes(tempFileName));
- System.IO.File.Delete(tempFileName);
- }
- if (string.IsNullOrWhiteSpace(DJoinResult))
- throw new InvalidOperationException(
-$@"Domain Join Unsuccessful
-Error: {stdError}
-Output: {stdOutput}");
-
- DiagnosticInformation = diagnosticInfo.ToString();
-
- // Reload Machine Account
- MachineAccount = RetrieveADMachineAccount($@"{Domain.NetBiosName}\{ComputerSamAccountName}", (MachineAccount == null ? null : MachineAccount.LoadedProperties.Keys.ToArray()));
-
- return DJoinResult;
- }
#endregion
public override string ToString()
diff --git a/Disco.Services/Interop/ActiveDirectory/ADMachineAccount.cs b/Disco.Services/Interop/ActiveDirectory/ADMachineAccount.cs
index 225f87f6..576a8b5b 100644
--- a/Disco.Services/Interop/ActiveDirectory/ADMachineAccount.cs
+++ b/Disco.Services/Interop/ActiveDirectory/ADMachineAccount.cs
@@ -204,6 +204,29 @@ namespace Disco.Services.Interop.ActiveDirectory
#region Actions
+ public void RenameAccount(ADDomainController writableDomainController, string newName)
+ {
+ if (IsCriticalSystemObject)
+ throw new InvalidOperationException($"This account [{DistinguishedName}] is a Critical System Active Directory Object and Disco ICT refuses to modify it");
+
+ if (!writableDomainController.IsWritable)
+ throw new InvalidOperationException($"The domain controller [{Name}] is not writable. This action (Delete Account) requires a writable domain controller.");
+
+ using (ADDirectoryEntry adEntry = writableDomainController.RetrieveDirectoryEntry(DistinguishedName))
+ {
+ var entry = adEntry.Entry;
+ entry.Properties["dNSHostName"][0] = $"{newName}.{Domain.Name}";
+ entry.Properties["sAMAccountName"][0] = $"{newName}$";
+ entry.CommitChanges();
+ entry.Rename($"CN={newName}");
+ entry.CommitChanges();
+
+ // Update Distinguished Name
+ Name = newName;
+ DistinguishedName = entry.Properties["distinguishedName"][0].ToString();
+ }
+ }
+
public void DeleteAccount(ADDomainController WritableDomainController)
{
if (IsCriticalSystemObject)
diff --git a/Disco.Services/Interop/ActiveDirectory/ActiveDirectory.cs b/Disco.Services/Interop/ActiveDirectory/ActiveDirectory.cs
index 4d525399..8f96102a 100644
--- a/Disco.Services/Interop/ActiveDirectory/ActiveDirectory.cs
+++ b/Disco.Services/Interop/ActiveDirectory/ActiveDirectory.cs
@@ -179,18 +179,6 @@ namespace Disco.Services.Interop.ActiveDirectory
}
#endregion
- #region Actions
-
- public static string OfflineDomainJoinProvision(string ComputerSamAccountName, string OrganisationalUnit, ref ADMachineAccount MachineAccount, out string DiagnosticInformation)
- {
- var domain = Context.GetDomainFromDistinguishedName(OrganisationalUnit);
- var writableDomainController = domain.GetAvailableDomainController(RequireWritable: true);
-
- return writableDomainController.OfflineDomainJoinProvision(ComputerSamAccountName, OrganisationalUnit, ref MachineAccount, out DiagnosticInformation);
- }
-
- #endregion
-
#region Helpers
public static string ParseDomainAccountId(string AccountId)