From 0f20b16f4139c631449466734eca0954d42a06b3 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Mon, 10 Oct 2016 20:07:20 +1100 Subject: [PATCH] Decommission AD descriptions #96 --- .../Devices/DeviceActionExtensions.cs | 4 +- Disco.Services/Disco.Services.csproj | 1 + .../ADDeviceDescriptionUpdateTask.cs | 138 ++++++++++++++++++ .../ActiveDirectory/ADMachineAccount.cs | 45 ++++-- Disco.Web/App_Start/AppConfig.cs | 4 + .../Areas/API/Controllers/SystemController.cs | 8 + .../T4MVC/API.SystemController.generated.cs | 13 ++ 7 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 Disco.Services/Interop/ActiveDirectory/ADDeviceDescriptionUpdateTask.cs diff --git a/Disco.Services/Devices/DeviceActionExtensions.cs b/Disco.Services/Devices/DeviceActionExtensions.cs index 59f5f2bb..7a898cab 100644 --- a/Disco.Services/Devices/DeviceActionExtensions.cs +++ b/Disco.Services/Devices/DeviceActionExtensions.cs @@ -80,7 +80,7 @@ namespace Disco.Services return true; } - public static void OnDecommission(this Device d, Disco.Models.Repository.DecommissionReasons Reason) + public static void OnDecommission(this Device d, DecommissionReasons Reason) { if (!d.CanDecommission()) throw new InvalidOperationException("Decommission of Device is Denied"); @@ -95,6 +95,7 @@ namespace Disco.Services if (adAccount != null && !adAccount.IsCriticalSystemObject) { adAccount.DisableAccount(); + adAccount.SetDescription(d); } } } @@ -122,6 +123,7 @@ namespace Disco.Services if (adAccount != null && !adAccount.IsCriticalSystemObject) { adAccount.EnableAccount(); + adAccount.SetDescription(d); } } } diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj index 40b3c644..e50a8f9e 100644 --- a/Disco.Services/Disco.Services.csproj +++ b/Disco.Services/Disco.Services.csproj @@ -322,6 +322,7 @@ + diff --git a/Disco.Services/Interop/ActiveDirectory/ADDeviceDescriptionUpdateTask.cs b/Disco.Services/Interop/ActiveDirectory/ADDeviceDescriptionUpdateTask.cs new file mode 100644 index 00000000..59fd1a39 --- /dev/null +++ b/Disco.Services/Interop/ActiveDirectory/ADDeviceDescriptionUpdateTask.cs @@ -0,0 +1,138 @@ +using Disco.Data.Repository; +using Disco.Services.Tasks; +using Quartz; +using System; +using System.IO; +using System.Linq; + +namespace Disco.Services.Interop.ActiveDirectory +{ + public class ADDeviceDescriptionUpdateTask : ScheduledTask + { + public override string TaskName { get { return "Active Directory Device Description Update"; } } + + public override bool SingleInstanceTask { get { return true; } } + public override bool CancelInitiallySupported { get { return false; } } + + #region Required Helpers + private static string RequiredFilePath(DiscoDataContext Database) + { + if (Database.DiscoConfiguration.DataStoreLocation != null) + return Path.Combine(Database.DiscoConfiguration.DataStoreLocation, "_ADDeviceDescriptionUpdateRequired.txt"); + else + return null; + } + + public static bool IsRequired(DiscoDataContext Database) + { + var requiredFilePath = RequiredFilePath(Database); + + if (requiredFilePath == null) + return false; + else + return File.Exists(requiredFilePath); + } + public static void SetRequired(DiscoDataContext Database) + { + var requiredFilePath = RequiredFilePath(Database); + + if (requiredFilePath != null) + { + File.WriteAllText(requiredFilePath, "This file exists to indicate an update to AD Device Descriptions is required. It will automatically be deleted when the update completes."); + File.SetAttributes(requiredFilePath, FileAttributes.Hidden); + } + } + #endregion + + public override void InitalizeScheduledTask(DiscoDataContext Database) + { + if (IsRequired(Database)) + { + // Schedule in 5mins + var trigger = TriggerBuilder.Create() + .StartAt(DateTimeOffset.Now.AddMinutes(3)); + + this.ScheduleTask(trigger); + } + } + + public static ScheduledTaskStatus ScheduleImmediately() + { + var existingTask = ScheduledTasks.GetTaskStatuses(typeof(ADDeviceDescriptionUpdateTask)).Where(s => s.IsRunning).FirstOrDefault(); + if (existingTask != null) + return existingTask; + + var instance = new ADDeviceDescriptionUpdateTask(); + return instance.ScheduleTask(); + } + + protected override void ExecuteTask() + { + using (DiscoDataContext database = new DiscoDataContext()) + { + Status.UpdateStatus(0, "Updating Active Directory Device Descriptions", "Reading Devices"); + + // Devices + var devices = database.Devices.Where(d => d.DeviceDomainId != null) + .ToList(); + + int failedTotal = 0; + int notFoundTotal = 0; + int completedTotal = 0; + + // Refine valid devices + devices = devices.Where(d => ActiveDirectory.IsValidDomainAccountId(d.DeviceDomainId)).ToList(); + + foreach (var domainGroup in devices.GroupBy(d => d.ComputerDomainName).ToList()) + { + ADDomain domain; + if (domainGroup.Key != null && ActiveDirectory.Context.TryGetDomainByNetBiosName(domainGroup.Key, out domain)) + { + var controller = domain.GetAvailableDomainController(RequireWritable: true); + + foreach (var device in domainGroup) + { + completedTotal++; + if ((completedTotal % 10) == 0) + { + Status.UpdateStatus((100D / devices.Count) * completedTotal, $"Processing: {device.DeviceDomainId} ({device.SerialNumber})"); + } + try + { + var adAccount = device.ActiveDirectoryAccount(); + if (adAccount == null) + { + notFoundTotal++; + + if (!device.DecommissionedDate.HasValue) + { + Status.LogWarning($"Unable to locate [{device.DeviceDomainId}] for commissioned device [{device.SerialNumber}] in the domain"); + } + } + else + { + adAccount.SetDescription(controller, device); + } + } + catch (Exception ex) + { + failedTotal++; + Status.LogWarning($"Error when setting description of computer account [{device.DeviceDomainId}] for device [{device.SerialNumber}]: [{ex.GetType().Name}] {ex.Message}"); + } + } + } + } + + // Finished - Remove Placeholder File + var requiredFilePath = RequiredFilePath(database); + if (requiredFilePath != null && File.Exists(requiredFilePath)) + File.Delete(requiredFilePath); + + Status.SetFinishedMessage($"Finished updating device descriptions for {devices.Count:N0}. {notFoundTotal:N0} were not found. {failedTotal:N0} failed."); + Status.LogInformation(Status.FinishedMessage); + Status.Finished(); + } + } + + } +} diff --git a/Disco.Services/Interop/ActiveDirectory/ADMachineAccount.cs b/Disco.Services/Interop/ActiveDirectory/ADMachineAccount.cs index fa87d456..66806c81 100644 --- a/Disco.Services/Interop/ActiveDirectory/ADMachineAccount.cs +++ b/Disco.Services/Interop/ActiveDirectory/ADMachineAccount.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Security.Principal; +using System.Text; namespace Disco.Services.Interop.ActiveDirectory { @@ -178,15 +179,18 @@ namespace Disco.Services.Interop.ActiveDirectory using (var deAccount = WritableDomainController.RetrieveDirectoryEntry(this.DistinguishedName)) { var descriptionProp = deAccount.Entry.Properties["description"]; - if (descriptionProp.Count > 0) + if (descriptionProp.Count != 1 || (descriptionProp[0] as string) != Description) { - descriptionProp.Clear(); + if (descriptionProp.Count > 0) + { + descriptionProp.Clear(); + } + if (!string.IsNullOrEmpty(Description)) + { + descriptionProp.Add(Description); + } + deAccount.Entry.CommitChanges(); } - if (!string.IsNullOrEmpty(Description)) - { - descriptionProp.Add(Description); - } - deAccount.Entry.CommitChanges(); } } public void SetDescription(string Description) @@ -196,19 +200,28 @@ namespace Disco.Services.Interop.ActiveDirectory public void SetDescription(ADDomainController WritableDomainController, Device Device) { - System.Text.StringBuilder descriptionBuilder = new System.Text.StringBuilder(); + var descriptionBuilder = new StringBuilder(); - if (Device.AssignedUserId != null) + if (Device.DecommissionedDate.HasValue) { - descriptionBuilder.Append(Device.AssignedUser.UserId).Append(" (").Append(Device.AssignedUser.DisplayName).Append("); "); + descriptionBuilder.Append("Decommissioned: ") + .Append(Device.DecommissionReason.ReasonMessage()) + .Append(" (").Append(Device.DecommissionedDate.Value.ToString("yyyy-MM-dd")).Append(')'); } - - if (Device.DeviceModelId.HasValue) + else { - descriptionBuilder.Append(Device.DeviceModel.Description).Append("; "); - } + if (Device.AssignedUserId != null) + { + descriptionBuilder.Append(Device.AssignedUser.UserId).Append(" (").Append(Device.AssignedUser.DisplayName).Append("); "); + } - descriptionBuilder.Append(Device.DeviceProfile.Description).Append(";"); + if (Device.DeviceModelId.HasValue) + { + descriptionBuilder.Append(Device.DeviceModel.Description).Append("; "); + } + + descriptionBuilder.Append(Device.DeviceProfile.Description).Append(";"); + } string description = descriptionBuilder.ToString().Trim(); if (description.Length > 1024) @@ -336,7 +349,7 @@ namespace Disco.Services.Interop.ActiveDirectory { i.Entry.UsePropertyCache = false; i.Entry.MoveTo(ou.Entry); - + // Update Distinguished Name this.DistinguishedName = i.Entry.Properties["distinguishedName"][0].ToString(); } diff --git a/Disco.Web/App_Start/AppConfig.cs b/Disco.Web/App_Start/AppConfig.cs index 421c4bef..fa0104ce 100644 --- a/Disco.Web/App_Start/AppConfig.cs +++ b/Disco.Web/App_Start/AppConfig.cs @@ -114,6 +114,10 @@ namespace Disco.Web // Attachment PDF Thumbnail Update if (PreviousVersion != null && PreviousVersion < new Version(2, 2, 0, 0)) Services.Documents.AttachmentImport.ThumbnailUpdateTask.SetRequired(Database); + + // AD Device Description Update + if (PreviousVersion != null && PreviousVersion < new Version(2, 2, 16281, 0)) + Services.Interop.ActiveDirectory.ADDeviceDescriptionUpdateTask.SetRequired(Database); } public static void DisposeEnvironment() diff --git a/Disco.Web/Areas/API/Controllers/SystemController.cs b/Disco.Web/Areas/API/Controllers/SystemController.cs index b610b3c2..fe58f457 100644 --- a/Disco.Web/Areas/API/Controllers/SystemController.cs +++ b/Disco.Web/Areas/API/Controllers/SystemController.cs @@ -33,6 +33,14 @@ namespace Disco.Web.Areas.API.Controllers return RedirectToAction(MVC.Config.Logging.TaskStatus(ts.SessionId)); } + [DiscoAuthorize(Claims.DiscoAdminAccount)] + public virtual ActionResult UpdateADDeviceDescriptions() + { + var ts = Disco.Services.Interop.ActiveDirectory.ADDeviceDescriptionUpdateTask.ScheduleImmediately(); + ts.SetFinishedUrl(Url.Action(MVC.Config.SystemConfig.Index())); + return RedirectToAction(MVC.Config.Logging.TaskStatus(ts.SessionId)); + } + [DiscoAuthorize(Claims.Config.System.Show)] public virtual ActionResult UpdateCheck() { diff --git a/Disco.Web/Extensions/T4MVC/API.SystemController.generated.cs b/Disco.Web/Extensions/T4MVC/API.SystemController.generated.cs index f0430318..ddb3cdd7 100644 --- a/Disco.Web/Extensions/T4MVC/API.SystemController.generated.cs +++ b/Disco.Web/Extensions/T4MVC/API.SystemController.generated.cs @@ -147,6 +147,7 @@ namespace Disco.Web.Areas.API.Controllers { public readonly string UpdateLastNetworkLogonDates = "UpdateLastNetworkLogonDates"; public readonly string UpdateAttachmentThumbnails = "UpdateAttachmentThumbnails"; + public readonly string UpdateADDeviceDescriptions = "UpdateADDeviceDescriptions"; public readonly string UpdateCheck = "UpdateCheck"; public readonly string UpdateOrganisationName = "UpdateOrganisationName"; public readonly string OrganisationLogo = "OrganisationLogo"; @@ -169,6 +170,7 @@ namespace Disco.Web.Areas.API.Controllers { public const string UpdateLastNetworkLogonDates = "UpdateLastNetworkLogonDates"; public const string UpdateAttachmentThumbnails = "UpdateAttachmentThumbnails"; + public const string UpdateADDeviceDescriptions = "UpdateADDeviceDescriptions"; public const string UpdateCheck = "UpdateCheck"; public const string UpdateOrganisationName = "UpdateOrganisationName"; public const string OrganisationLogo = "OrganisationLogo"; @@ -349,6 +351,17 @@ namespace Disco.Web.Areas.API.Controllers return callInfo; } + [NonAction] + partial void UpdateADDeviceDescriptionsOverride(T4MVC_System_Web_Mvc_ActionResult callInfo); + + [NonAction] + public override System.Web.Mvc.ActionResult UpdateADDeviceDescriptions() + { + var callInfo = new T4MVC_System_Web_Mvc_ActionResult(Area, Name, ActionNames.UpdateADDeviceDescriptions); + UpdateADDeviceDescriptionsOverride(callInfo); + return callInfo; + } + [NonAction] partial void UpdateCheckOverride(T4MVC_System_Web_Mvc_ActionResult callInfo);