Feature #49: Active Directory Managed Groups
Document Template Attachments, Device Batches, Device Profiles and User Flags can be associated with an Active Directory group. This AD group is then automatically synchronized with relevant User/Machine accounts. Contains various other UI tweaks and configuration enhancements.
This commit is contained in:
@@ -193,26 +193,34 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
#endregion
|
||||
|
||||
#region Groups
|
||||
public ADGroup RetrieveADGroup(string Id)
|
||||
public ADGroup RetrieveADGroup(string Id, string[] AdditionalProperties = null)
|
||||
{
|
||||
var result = RetrieveBySamAccountName(Id, ADGroup.LdapSamAccountNameFilterTemplate, ADGroup.LoadProperties);
|
||||
string[] loadProperites = (AdditionalProperties != null && AdditionalProperties.Length > 0)
|
||||
? ADGroup.LoadProperties.Concat(AdditionalProperties).ToArray()
|
||||
: ADGroup.LoadProperties;
|
||||
|
||||
var result = RetrieveBySamAccountName(Id, ADGroup.LdapSamAccountNameFilterTemplate, loadProperites);
|
||||
|
||||
if (result == null)
|
||||
return null;
|
||||
else
|
||||
return result.AsADGroup();
|
||||
return result.AsADGroup(AdditionalProperties);
|
||||
}
|
||||
public ADGroup RetrieveADGroupByDistinguishedName(string DistinguishedName)
|
||||
public ADGroup RetrieveADGroupByDistinguishedName(string DistinguishedName, string[] AdditionalProperties = null)
|
||||
{
|
||||
using (var groupEntry = this.RetrieveDirectoryEntry(DistinguishedName, ADGroup.LoadProperties))
|
||||
string[] loadProperites = (AdditionalProperties != null && AdditionalProperties.Length > 0)
|
||||
? ADGroup.LoadProperties.Concat(AdditionalProperties).ToArray()
|
||||
: ADGroup.LoadProperties;
|
||||
|
||||
using (var groupEntry = this.RetrieveDirectoryEntry(DistinguishedName, loadProperites))
|
||||
{
|
||||
if (groupEntry == null)
|
||||
return null;
|
||||
|
||||
return groupEntry.AsADGroup();
|
||||
return groupEntry.AsADGroup(AdditionalProperties);
|
||||
}
|
||||
}
|
||||
public ADGroup RetrieveADGroupWithSecurityIdentifier(SecurityIdentifier SecurityIdentifier)
|
||||
public ADGroup RetrieveADGroupWithSecurityIdentifier(SecurityIdentifier SecurityIdentifier, string[] AdditionalProperties = null)
|
||||
{
|
||||
if (SecurityIdentifier == null)
|
||||
throw new ArgumentNullException("SecurityIdentifier");
|
||||
@@ -222,12 +230,15 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
var sidBinaryString = SecurityIdentifier.ToBinaryString();
|
||||
|
||||
string ldapFilter = string.Format(ADGroup.LdapSecurityIdentifierFilterTemplate, sidBinaryString);
|
||||
string[] loadProperites = (AdditionalProperties != null && AdditionalProperties.Length > 0)
|
||||
? ADGroup.LoadProperties.Concat(AdditionalProperties).ToArray()
|
||||
: ADGroup.LoadProperties;
|
||||
|
||||
var result = this.SearchEntireDomain(ldapFilter, ADGroup.LoadProperties, ActiveDirectory.SingleSearchResult).FirstOrDefault();
|
||||
var result = this.SearchEntireDomain(ldapFilter, loadProperites, ActiveDirectory.SingleSearchResult).FirstOrDefault();
|
||||
if (result == null)
|
||||
return null;
|
||||
else
|
||||
return result.AsADGroup();
|
||||
return result.AsADGroup(AdditionalProperties);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -236,7 +247,7 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
private static readonly string[] ObjectLoadProperties = { "objectCategory" };
|
||||
private static readonly string[] ObjectLoadPropertiesAll = ObjectLoadProperties.Concat(ADUserAccount.LoadProperties).Concat(ADMachineAccount.LoadProperties).Concat(ADGroup.LoadProperties).Distinct().ToArray();
|
||||
|
||||
public IADObject RetrieveADObject(string Id, bool Quick)
|
||||
public IADObject RetrieveADObject(string Id, bool Quick, string[] AdditionalProperties = null)
|
||||
{
|
||||
var result = RetrieveBySamAccountName(Id, ObjectLdapSamAccountNameFilter, ObjectLoadPropertiesAll);
|
||||
|
||||
@@ -249,11 +260,11 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
switch (objectCategory)
|
||||
{
|
||||
case "cn=person":
|
||||
return result.AsADUserAccount(Quick, null);
|
||||
return result.AsADUserAccount(Quick, AdditionalProperties);
|
||||
case "cn=computer":
|
||||
return result.AsADMachineAccount(null);
|
||||
return result.AsADMachineAccount(AdditionalProperties);
|
||||
case "cn=group":
|
||||
return result.AsADGroup();
|
||||
return result.AsADGroup(AdditionalProperties);
|
||||
default:
|
||||
throw new InvalidOperationException("Unexpected objectCategory");
|
||||
}
|
||||
@@ -294,12 +305,12 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
|
||||
private ADSearchResult RetrieveBySamAccountName(string Id, string LdapFilterTemplate, string[] LoadProperties)
|
||||
{
|
||||
var splitId = UserExtensions.SplitUserId(Id);
|
||||
var slashIndex = Id.IndexOf('\\');
|
||||
|
||||
if (!this.Domain.NetBiosName.Equals(splitId.Item1, StringComparison.OrdinalIgnoreCase))
|
||||
if (!this.Domain.NetBiosName.Equals(Id.Substring(0, slashIndex), StringComparison.OrdinalIgnoreCase))
|
||||
throw new ArgumentException(string.Format("The Id [{0}] is invalid for this domain [{1}]", Id, this.Domain.Name), "Id");
|
||||
|
||||
var ldapFilter = string.Format(LdapFilterTemplate, splitId.Item2);
|
||||
var ldapFilter = string.Format(LdapFilterTemplate, Id.Substring(slashIndex + 1));
|
||||
|
||||
return this.SearchEntireDomain(ldapFilter, LoadProperties, ActiveDirectory.SingleSearchResult).FirstOrDefault();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
|
||||
public string DistinguishedName { get; private set; }
|
||||
public SecurityIdentifier SecurityIdentifier { get; private set; }
|
||||
|
||||
|
||||
public string Id { get { return string.Format(@"{0}\{1}", Domain.NetBiosName, SamAccountName); } }
|
||||
public string SamAccountName { get; private set; }
|
||||
|
||||
@@ -26,7 +26,9 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
|
||||
public List<string> MemberOf { get; private set; }
|
||||
|
||||
private ADGroup(ADDomain Domain, string DistinguishedName, SecurityIdentifier SecurityIdentifier, string SamAccountName, string Name, List<string> MemberOf)
|
||||
public Dictionary<string, object[]> LoadedProperties { get; private set; }
|
||||
|
||||
private ADGroup(ADDomain Domain, string DistinguishedName, SecurityIdentifier SecurityIdentifier, string SamAccountName, string Name, List<string> MemberOf, Dictionary<string, object[]> LoadedProperties)
|
||||
{
|
||||
this.Domain = Domain;
|
||||
this.DistinguishedName = DistinguishedName;
|
||||
@@ -34,9 +36,10 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
this.SamAccountName = SamAccountName;
|
||||
this.Name = Name;
|
||||
this.MemberOf = MemberOf;
|
||||
this.LoadedProperties = LoadedProperties;
|
||||
}
|
||||
|
||||
public static ADGroup FromSearchResult(ADSearchResult SearchResult)
|
||||
public static ADGroup FromSearchResult(ADSearchResult SearchResult, string[] AdditionalProperties)
|
||||
{
|
||||
if (SearchResult == null)
|
||||
throw new ArgumentNullException("SearchResult");
|
||||
@@ -47,10 +50,21 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
var objectSid = new SecurityIdentifier(SearchResult.Value<byte[]>("objectSid"), 0);
|
||||
var memberOf = SearchResult.Values<string>("memberOf").ToList();
|
||||
|
||||
return new ADGroup(SearchResult.Domain, distinguishedName, objectSid, sAMAccountName, name, memberOf);
|
||||
// Additional Properties
|
||||
Dictionary<string, object[]> additionalProperties;
|
||||
if (AdditionalProperties != null)
|
||||
additionalProperties = AdditionalProperties
|
||||
.Select(p => Tuple.Create(p, SearchResult.Values<object>(p).ToArray()))
|
||||
.ToDictionary(t => t.Item1, t => t.Item2);
|
||||
else
|
||||
{
|
||||
additionalProperties = new Dictionary<string, object[]>();
|
||||
}
|
||||
|
||||
return new ADGroup(SearchResult.Domain, distinguishedName, objectSid, sAMAccountName, name, memberOf, additionalProperties);
|
||||
}
|
||||
|
||||
public static ADGroup FromDirectoryEntry(ADDirectoryEntry DirectoryEntry)
|
||||
public static ADGroup FromDirectoryEntry(ADDirectoryEntry DirectoryEntry, string[] AdditionalProperties)
|
||||
{
|
||||
if (DirectoryEntry == null)
|
||||
throw new ArgumentNullException("DirectoryEntry");
|
||||
@@ -63,7 +77,50 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
var objectSid = new SecurityIdentifier(properties.Value<byte[]>("objectSid"), 0);
|
||||
var memberOf = properties.Values<string>("memberOf").ToList();
|
||||
|
||||
return new ADGroup(DirectoryEntry.Domain, distinguishedName, objectSid, sAMAccountName, name, memberOf);
|
||||
Dictionary<string, object[]> additionalProperties;
|
||||
if (AdditionalProperties != null)
|
||||
additionalProperties = AdditionalProperties
|
||||
.Select(p => Tuple.Create(p, properties.Values<object>(p).ToArray()))
|
||||
.ToDictionary(t => t.Item1, t => t.Item2);
|
||||
else
|
||||
{
|
||||
additionalProperties = new Dictionary<string, object[]>();
|
||||
}
|
||||
|
||||
return new ADGroup(DirectoryEntry.Domain, distinguishedName, objectSid, sAMAccountName, name, memberOf, additionalProperties);
|
||||
}
|
||||
|
||||
[Obsolete("Use generic equivalents: GetPropertyValue<T>(string PropertyName)")]
|
||||
public object GetPropertyValue(string PropertyName, int Index = 0)
|
||||
{
|
||||
return GetPropertyValues<object>(PropertyName).Skip(Index).FirstOrDefault();
|
||||
}
|
||||
|
||||
public T GetPropertyValue<T>(string PropertyName)
|
||||
{
|
||||
return GetPropertyValues<T>(PropertyName).FirstOrDefault();
|
||||
}
|
||||
public IEnumerable<T> GetPropertyValues<T>(string PropertyName)
|
||||
{
|
||||
switch (PropertyName.ToLower())
|
||||
{
|
||||
case "name":
|
||||
return new string[] { this.Name }.OfType<T>();
|
||||
case "samaccountname":
|
||||
return new string[] { this.SamAccountName }.OfType<T>();
|
||||
case "distinguishedname":
|
||||
return new string[] { this.DistinguishedName }.OfType<T>();
|
||||
case "objectsid":
|
||||
return new SecurityIdentifier[] { this.SecurityIdentifier }.OfType<T>();
|
||||
case "memberof":
|
||||
return this.MemberOf.OfType<T>();
|
||||
default:
|
||||
object[] adProperty;
|
||||
if (this.LoadedProperties.TryGetValue(PropertyName, out adProperty))
|
||||
return adProperty.OfType<T>();
|
||||
else
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
@@ -104,26 +104,35 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
};
|
||||
}
|
||||
|
||||
[Obsolete("Use generic equivalents: GetPropertyValue<T>(string PropertyName)")]
|
||||
public object GetPropertyValue(string PropertyName, int Index = 0)
|
||||
{
|
||||
return GetPropertyValues<object>(PropertyName).Skip(Index).FirstOrDefault();
|
||||
}
|
||||
public T GetPropertyValue<T>(string PropertyName)
|
||||
{
|
||||
return GetPropertyValues<T>(PropertyName).FirstOrDefault();
|
||||
}
|
||||
public IEnumerable<T> GetPropertyValues<T>(string PropertyName)
|
||||
{
|
||||
switch (PropertyName.ToLower())
|
||||
{
|
||||
case "name":
|
||||
return this.Name;
|
||||
return new string[] { this.Name }.OfType<T>();
|
||||
case "samaccountname":
|
||||
return this.SamAccountName;
|
||||
return new string[] { this.SamAccountName }.OfType<T>();
|
||||
case "distinguishedname":
|
||||
return this.DistinguishedName;
|
||||
return new string[] { this.DistinguishedName }.OfType<T>();
|
||||
case "objectsid":
|
||||
return this.SecurityIdentifier.ToString();
|
||||
return new SecurityIdentifier[] { this.SecurityIdentifier }.OfType<T>();
|
||||
case "netbootguid":
|
||||
return this.NetbootGUID;
|
||||
return new Guid[] { this.NetbootGUID }.OfType<T>();
|
||||
default:
|
||||
object[] adProperty;
|
||||
if (this.LoadedProperties.TryGetValue(PropertyName, out adProperty) && Index <= adProperty.Length)
|
||||
return adProperty[Index];
|
||||
if (this.LoadedProperties.TryGetValue(PropertyName, out adProperty))
|
||||
return adProperty.OfType<T>();
|
||||
else
|
||||
return null;
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.Services.Interop.ActiveDirectory;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Disco.Services.Interop.ActiveDirectory
|
||||
{
|
||||
public abstract class ADManagedGroup : IDisposable
|
||||
{
|
||||
public string Key { get; private set; }
|
||||
public ADManagedGroupConfiguration Configuration { get; private set; }
|
||||
|
||||
internal ActiveDirectoryManagedGroups Context { get; set; }
|
||||
|
||||
public abstract string Description { get; }
|
||||
public abstract string CategoryDescription { get; }
|
||||
public abstract string GroupDescription { get; }
|
||||
public abstract bool IncludeFilterBeginDate { get; }
|
||||
|
||||
public ADManagedGroup(string Key, ADManagedGroupConfiguration Configuration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Key))
|
||||
throw new ArgumentNullException("Key");
|
||||
if (Configuration == null)
|
||||
throw new ArgumentNullException("Configuration");
|
||||
if (!ActiveDirectory.IsValidDomainAccountId(Configuration.GroupId))
|
||||
throw new ArgumentException("Configuration.GroupId is not a valid Domain Account Id", "Configuration");
|
||||
|
||||
this.Key = Key;
|
||||
this.Configuration = Configuration;
|
||||
}
|
||||
|
||||
public abstract void Initialize();
|
||||
public abstract IEnumerable<string> DetermineMembers(DiscoDataContext Database);
|
||||
|
||||
public ADGroup GetGroup()
|
||||
{
|
||||
return ActiveDirectory.RetrieveADGroup(this.Configuration.GroupId, "member");
|
||||
}
|
||||
|
||||
protected void AddMember(string Id)
|
||||
{
|
||||
AddMember(Id, null);
|
||||
}
|
||||
protected void AddMember(string InvokingIdentifier, Func<DiscoDataContext, IEnumerable<string>> MemberResolver)
|
||||
{
|
||||
if (Context == null)
|
||||
return; // Must be added to ActiveDirectoryManagedGroups
|
||||
|
||||
var action = new ADManagedGroupScheduledAction(
|
||||
this,
|
||||
ADManagedGroupScheduledActionType.AddGroupMember,
|
||||
InvokingIdentifier,
|
||||
MemberResolver);
|
||||
|
||||
Context.ScheduleAction(action);
|
||||
}
|
||||
|
||||
protected void RemoveMember(string Id)
|
||||
{
|
||||
RemoveMember(Id, null);
|
||||
}
|
||||
protected void RemoveMember(string InvokingIdentifier, Func<DiscoDataContext, IEnumerable<string>> MemberResolver)
|
||||
{
|
||||
if (Context == null)
|
||||
return; // Must be added to ActiveDirectoryManagedGroups
|
||||
|
||||
var action = new ADManagedGroupScheduledAction(
|
||||
this,
|
||||
ADManagedGroupScheduledActionType.RemoveGroupMember,
|
||||
InvokingIdentifier,
|
||||
MemberResolver);
|
||||
|
||||
Context.ScheduleAction(action);
|
||||
}
|
||||
|
||||
public static ADManagedGroupConfiguration ConfigurationFromJson(string ConfigurationJson)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<ADManagedGroupConfiguration>(ConfigurationJson);
|
||||
}
|
||||
public static string ValidConfigurationToJson(string GroupKey, string GroupId, DateTime? FilterBeginDate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(GroupId))
|
||||
GroupId = null;
|
||||
|
||||
if (GroupId != null)
|
||||
GroupId = ActiveDirectory.Context.ManagedGroups.ValidateGroupId(GroupId, GroupKey);
|
||||
|
||||
if (GroupId == null)
|
||||
return null;
|
||||
else
|
||||
return JsonConvert.SerializeObject(new ADManagedGroupConfiguration()
|
||||
{
|
||||
GroupId = GroupId,
|
||||
FilterBeginDate = FilterBeginDate
|
||||
}, new JsonSerializerSettings() { DefaultValueHandling = DefaultValueHandling.Ignore });
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Services.Logging;
|
||||
using Disco.Services.Tasks;
|
||||
using Quartz;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Disco.Services.Interop.ActiveDirectory
|
||||
{
|
||||
public class ADManagedGroupsSyncTask : ScheduledTask
|
||||
{
|
||||
public override string TaskName { get { return "Active Directory - Synchronise Managed Groups"; } }
|
||||
public override bool SingleInstanceTask { get { return true; } }
|
||||
public override bool CancelInitiallySupported { get { return false; } }
|
||||
|
||||
public override void InitalizeScheduledTask(DiscoDataContext Database)
|
||||
{
|
||||
// ADManagedGroupsSyncTask @ 11:00pm
|
||||
TriggerBuilder triggerBuilder = TriggerBuilder.Create().
|
||||
WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(23, 0));
|
||||
|
||||
this.ScheduleTask(triggerBuilder);
|
||||
}
|
||||
|
||||
protected override void ExecuteTask()
|
||||
{
|
||||
int changeCount;
|
||||
|
||||
List<ADManagedGroup> managedGroups = this.ExecutionContext.JobDetail.JobDataMap["ManagedGroups"] as List<ADManagedGroup>;
|
||||
if (managedGroups == null)
|
||||
managedGroups = ActiveDirectory.Context.ManagedGroups.Values;
|
||||
|
||||
this.Status.UpdateStatus(0, "Synchronising Active Directory Managed Groups", "Starting");
|
||||
|
||||
changeCount = ActiveDirectory.Context.ManagedGroups.SyncManagedGroups(managedGroups, this.Status);
|
||||
|
||||
SystemLog.LogInformation(new string[]
|
||||
{
|
||||
"Synchronised Active Directory Managed Groups",
|
||||
changeCount.ToString()
|
||||
});
|
||||
this.Status.SetFinishedMessage(string.Format("Made {0} Changes to Active Directory Groups", changeCount));
|
||||
}
|
||||
|
||||
public static ScheduledTaskStatus ScheduleSync(ADManagedGroup ManagedGroup)
|
||||
{
|
||||
if (ManagedGroup == null)
|
||||
throw new ArgumentNullException("ManagedGroup");
|
||||
|
||||
JobDataMap taskData = new JobDataMap() {
|
||||
{"ManagedGroups", new List<ADManagedGroup> { ManagedGroup } }
|
||||
};
|
||||
|
||||
var instance = new ADManagedGroupsSyncTask();
|
||||
return instance.ScheduleTask(taskData);
|
||||
}
|
||||
public static ScheduledTaskStatus ScheduleSync(IEnumerable<ADManagedGroup> ManagedGroups)
|
||||
{
|
||||
if (ManagedGroups == null)
|
||||
throw new ArgumentNullException("ManagedGroups");
|
||||
|
||||
JobDataMap taskData = new JobDataMap() {
|
||||
{"ManagedGroups", ManagedGroups.ToList() }
|
||||
};
|
||||
|
||||
var instance = new ADManagedGroupsSyncTask();
|
||||
return instance.ScheduleTask(taskData);
|
||||
}
|
||||
public static ScheduledTaskStatus ScheduleSyncAll()
|
||||
{
|
||||
var instance = new ADManagedGroupsSyncTask();
|
||||
return instance.ScheduleTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-10
@@ -12,16 +12,15 @@ using System.Linq;
|
||||
|
||||
namespace Disco.Services.Interop.ActiveDirectory
|
||||
{
|
||||
public class ADTaskUpdateNetworkLogonDates : ScheduledTask
|
||||
public class ADNetworkLogonDatesUpdateTask : ScheduledTask
|
||||
{
|
||||
|
||||
public override string TaskName { get { return "Active Directory - Update Last Network Logon Dates Task"; } }
|
||||
public override bool SingleInstanceTask { get { return true; } }
|
||||
public override bool CancelInitiallySupported { get { return false; } }
|
||||
|
||||
public override void InitalizeScheduledTask(DiscoDataContext Database)
|
||||
{
|
||||
// ActiveDirectoryUpdateLastNetworkLogonDateJob @ 11:30pm
|
||||
// ADNetworkLogonDatesUpdateTask @ 11:30pm
|
||||
TriggerBuilder triggerBuilder = TriggerBuilder.Create().
|
||||
WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(23, 30));
|
||||
|
||||
@@ -50,11 +49,11 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
|
||||
public static ScheduledTaskStatus ScheduleImmediately()
|
||||
{
|
||||
var existingTask = ScheduledTasks.GetTaskStatuses(typeof(ADTaskUpdateNetworkLogonDates)).Where(s => s.IsRunning).FirstOrDefault();
|
||||
var existingTask = ScheduledTasks.GetTaskStatuses(typeof(ADNetworkLogonDatesUpdateTask)).Where(s => s.IsRunning).FirstOrDefault();
|
||||
if (existingTask != null)
|
||||
return existingTask;
|
||||
|
||||
var instance = new ADTaskUpdateNetworkLogonDates();
|
||||
var instance = new ADNetworkLogonDatesUpdateTask();
|
||||
return instance.ScheduleTask();
|
||||
}
|
||||
|
||||
@@ -68,16 +67,18 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
if (!string.IsNullOrEmpty(Device.DeviceDomainId) && Device.DeviceDomainId.Contains('\\'))
|
||||
{
|
||||
var context = ActiveDirectory.Context;
|
||||
var deviceSamAccountName = UserExtensions.SplitUserId(Device.DeviceDomainId).Item2 + "$";
|
||||
var ldapFilter = string.Format(ldapFilterTemplate, ADHelpers.EscapeLdapQuery(deviceSamAccountName));
|
||||
string deviceSamAccountName;
|
||||
ADDomain deviceDomain;
|
||||
|
||||
var domain = context.GetDomainFromId(Device.DeviceDomainId);
|
||||
ActiveDirectory.ParseDomainAccountId(Device.DeviceDomainId + "$", out deviceSamAccountName, out deviceDomain);
|
||||
|
||||
var ldapFilter = string.Format(ldapFilterTemplate, ADHelpers.EscapeLdapQuery(deviceSamAccountName));
|
||||
IEnumerable<ADDomainController> domainControllers;
|
||||
|
||||
if (context.SearchAllForestServers)
|
||||
domainControllers = domain.GetAllReachableDomainControllers();
|
||||
domainControllers = deviceDomain.GetAllReachableDomainControllers();
|
||||
else
|
||||
domainControllers = domain.GetReachableSiteDomainControllers();
|
||||
domainControllers = deviceDomain.GetReachableSiteDomainControllers();
|
||||
|
||||
lastLogon = domainControllers.Select(dc =>
|
||||
{
|
||||
@@ -111,32 +111,41 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
additionalProperties);
|
||||
}
|
||||
|
||||
[Obsolete("Use generic equivalents: GetPropertyValue<T>(string PropertyName)")]
|
||||
public object GetPropertyValue(string PropertyName, int Index = 0)
|
||||
{
|
||||
return GetPropertyValues<object>(PropertyName).Skip(Index).FirstOrDefault();
|
||||
}
|
||||
public T GetPropertyValue<T>(string PropertyName)
|
||||
{
|
||||
return GetPropertyValues<T>(PropertyName).FirstOrDefault();
|
||||
}
|
||||
public IEnumerable<T> GetPropertyValues<T>(string PropertyName)
|
||||
{
|
||||
switch (PropertyName.ToLower())
|
||||
{
|
||||
case "name":
|
||||
return this.Name;
|
||||
return new string[] { this.Name }.OfType<T>();
|
||||
case "samaccountname":
|
||||
return this.SamAccountName;
|
||||
return new string[] { this.SamAccountName }.OfType<T>();
|
||||
case "distinguishedname":
|
||||
return this.DistinguishedName;
|
||||
return new string[] { this.DistinguishedName }.OfType<T>();
|
||||
case "objectsid":
|
||||
return this.SecurityIdentifier.ToString();
|
||||
return new SecurityIdentifier[] { this.SecurityIdentifier }.OfType<T>();
|
||||
case "sn":
|
||||
return this.Surname;
|
||||
return new string[] { this.Surname }.OfType<T>();
|
||||
case "givenname":
|
||||
return this.GivenName;
|
||||
return new string[] { this.GivenName }.OfType<T>();
|
||||
case "mail":
|
||||
return this.Email;
|
||||
return new string[] { this.Email }.OfType<T>();
|
||||
case "telephonenumber":
|
||||
return this.Phone;
|
||||
return new string[] { this.Phone }.OfType<T>();
|
||||
default:
|
||||
object[] adProperty;
|
||||
if (this.LoadedProperties.TryGetValue(PropertyName, out adProperty) && Index <= adProperty.Length)
|
||||
return adProperty[Index];
|
||||
if (this.LoadedProperties.TryGetValue(PropertyName, out adProperty))
|
||||
return adProperty.OfType<T>();
|
||||
else
|
||||
return null;
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,23 +114,23 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
|
||||
#region Groups
|
||||
|
||||
public static ADGroup RetrieveADGroup(string Id)
|
||||
public static ADGroup RetrieveADGroup(string Id, params string[] AdditionalProperties)
|
||||
{
|
||||
var domain = Context.GetDomainFromId(Id);
|
||||
return domain.GetAvailableDomainController().RetrieveADGroup(Id);
|
||||
return domain.GetAvailableDomainController().RetrieveADGroup(Id, AdditionalProperties);
|
||||
}
|
||||
public static ADGroup RetrieveADGroupByDistinguishedName(string DistinguishedName)
|
||||
public static ADGroup RetrieveADGroupByDistinguishedName(string DistinguishedName, params string[] AdditionalProperties)
|
||||
{
|
||||
var domain = Context.GetDomainFromDistinguishedName(DistinguishedName);
|
||||
return domain.GetAvailableDomainController().RetrieveADGroupByDistinguishedName(DistinguishedName);
|
||||
return domain.GetAvailableDomainController().RetrieveADGroupByDistinguishedName(DistinguishedName, AdditionalProperties);
|
||||
}
|
||||
public static ADGroup RetrieveADGroupWithSecurityIdentifier(SecurityIdentifier SecurityIdentifier)
|
||||
public static ADGroup RetrieveADGroupWithSecurityIdentifier(SecurityIdentifier SecurityIdentifier, params string[] AdditionalProperties)
|
||||
{
|
||||
var domain = Context.GetDomainFromSecurityIdentifier(SecurityIdentifier);
|
||||
return domain.GetAvailableDomainController().RetrieveADGroupWithSecurityIdentifier(SecurityIdentifier);
|
||||
return domain.GetAvailableDomainController().RetrieveADGroupWithSecurityIdentifier(SecurityIdentifier, AdditionalProperties);
|
||||
}
|
||||
|
||||
public static IEnumerable<ADGroup> SearchADGroups(string Term, int? ResultLimit = ActiveDirectory.DefaultSearchResultLimit)
|
||||
public static IEnumerable<ADGroup> SearchADGroups(string Term, int? ResultLimit = ActiveDirectory.DefaultSearchResultLimit, params string[] AdditionalProperties)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Term))
|
||||
throw new ArgumentNullException("Term");
|
||||
@@ -141,7 +141,7 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
if (string.IsNullOrWhiteSpace(term))
|
||||
return Enumerable.Empty<ADGroup>();
|
||||
|
||||
var ldapFilter= string.Format(ADGroup.LdapSearchFilterTemplate, ADHelpers.EscapeLdapQuery(term));
|
||||
var ldapFilter = string.Format(ADGroup.LdapSearchFilterTemplate, ADHelpers.EscapeLdapQuery(term));
|
||||
|
||||
IEnumerable<ADSearchResult> searchResults;
|
||||
if (searchDomain != null)
|
||||
@@ -149,7 +149,7 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
else
|
||||
searchResults = Context.SearchScope(ldapFilter, ADGroup.LoadProperties, ResultLimit);
|
||||
|
||||
return searchResults.Select(result => result.AsADGroup());
|
||||
return searchResults.Select(result => result.AsADGroup(AdditionalProperties));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -185,6 +185,124 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
|
||||
#region Helpers
|
||||
|
||||
public static string ParseDomainAccountId(string AccountId)
|
||||
{
|
||||
return ParseDomainAccountId(AccountId, null);
|
||||
}
|
||||
public static string ParseDomainAccountId(string AccountId, string AccountDomain)
|
||||
{
|
||||
string accountUsername;
|
||||
ADDomain domain;
|
||||
|
||||
return ParseDomainAccountId(AccountId, AccountDomain, out accountUsername, out domain);
|
||||
}
|
||||
public static string ParseDomainAccountId(string AccountId, out string AccountUsername)
|
||||
{
|
||||
return ParseDomainAccountId(AccountId, null, out AccountUsername);
|
||||
}
|
||||
public static string ParseDomainAccountId(string AccountId, string AccountDomain, out string AccountUsername)
|
||||
{
|
||||
ADDomain domain;
|
||||
|
||||
return ParseDomainAccountId(AccountId, AccountDomain, out AccountUsername, out domain);
|
||||
}
|
||||
public static string ParseDomainAccountId(string AccountId, out ADDomain Domain)
|
||||
{
|
||||
return ParseDomainAccountId(AccountId, null, out Domain);
|
||||
}
|
||||
public static string ParseDomainAccountId(string AccountId, string AccountDomain, out ADDomain Domain)
|
||||
{
|
||||
string accountUsername;
|
||||
|
||||
return ParseDomainAccountId(AccountId, AccountDomain, out accountUsername, out Domain);
|
||||
}
|
||||
public static string ParseDomainAccountId(string AccountId, out string AccountUsername, out ADDomain Domain)
|
||||
{
|
||||
return ParseDomainAccountId(AccountId, null, out AccountUsername, out Domain);
|
||||
}
|
||||
public static string ParseDomainAccountId(string AccountId, string AccountDomain, out string AccountUsername, out ADDomain Domain)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(AccountId))
|
||||
throw new ArgumentNullException("AccountId");
|
||||
|
||||
var slashIndex = AccountId.IndexOf('\\');
|
||||
|
||||
if (slashIndex < 0 && !string.IsNullOrWhiteSpace(AccountDomain))
|
||||
{
|
||||
AccountId = AccountDomain + @"\" + AccountId;
|
||||
slashIndex = AccountDomain.Length;
|
||||
}
|
||||
|
||||
if (slashIndex < 0)
|
||||
{
|
||||
AccountUsername = AccountId;
|
||||
Domain = Context.PrimaryDomain;
|
||||
}
|
||||
else
|
||||
{
|
||||
AccountUsername = AccountId.Substring(slashIndex + 1);
|
||||
Domain = Context.GetDomainByNetBiosName(AccountId.Substring(0, slashIndex));
|
||||
}
|
||||
|
||||
return string.Concat(Domain.NetBiosName, @"\", AccountUsername);
|
||||
}
|
||||
|
||||
public static bool IsValidDomainAccountId(string AccountId)
|
||||
{
|
||||
string accountUsername;
|
||||
ADDomain domain;
|
||||
|
||||
return IsValidDomainAccountId(AccountId, out accountUsername, out domain);
|
||||
}
|
||||
public static bool IsValidDomainAccountId(string AccountId, out string AccountUsername)
|
||||
{
|
||||
ADDomain domain;
|
||||
|
||||
return IsValidDomainAccountId(AccountId, out AccountUsername, out domain);
|
||||
}
|
||||
public static bool IsValidDomainAccountId(string AccountId, out ADDomain Domain)
|
||||
{
|
||||
string accountUsername;
|
||||
|
||||
return IsValidDomainAccountId(AccountId, out accountUsername, out Domain);
|
||||
}
|
||||
public static bool IsValidDomainAccountId(string AccountId, out string AccountUsername, out ADDomain Domain)
|
||||
{
|
||||
if (string.IsNullOrEmpty(AccountId))
|
||||
{
|
||||
AccountUsername = null;
|
||||
Domain = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var slashIndex = AccountId.IndexOf('\\');
|
||||
if (slashIndex < 0)
|
||||
{
|
||||
AccountUsername = AccountId;
|
||||
Domain = null;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
AccountUsername = AccountId.Substring(slashIndex + 1);
|
||||
return ActiveDirectory.Context.TryGetDomainByNetBiosName(AccountId.Substring(0, slashIndex), out Domain);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the AccountId Domain matches the Primary Domain, returns the Account Username without the Domain specified
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string FriendlyAccountId(string AccountId)
|
||||
{
|
||||
var slashIndex = AccountId.IndexOf('\\');
|
||||
|
||||
if (slashIndex > 0 && AccountId.Substring(0, slashIndex).Equals(ActiveDirectory.Context.PrimaryDomain.NetBiosName, StringComparison.OrdinalIgnoreCase))
|
||||
return AccountId.Substring(slashIndex + 1);
|
||||
else
|
||||
return AccountId;
|
||||
}
|
||||
|
||||
private static string RelevantSearchTerm(string Term, out ADDomain Domain)
|
||||
{
|
||||
Domain = null;
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
public ADSite Site { get; private set; }
|
||||
public ADDomain PrimaryDomain { get; private set; }
|
||||
public List<ADDomain> Domains { get; private set; }
|
||||
public ActiveDirectoryManagedGroups ManagedGroups { get; private set; }
|
||||
|
||||
public List<string> ForestServers
|
||||
{
|
||||
@@ -39,7 +40,12 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
|
||||
#region Contructor/Initializing
|
||||
|
||||
internal ActiveDirectoryContext(DiscoDataContext Database)
|
||||
private ActiveDirectoryContext()
|
||||
{
|
||||
ManagedGroups = new ActiveDirectoryManagedGroups();
|
||||
}
|
||||
|
||||
internal ActiveDirectoryContext(DiscoDataContext Database) : this()
|
||||
{
|
||||
Initialize(Database);
|
||||
}
|
||||
@@ -138,24 +144,24 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
if (string.IsNullOrWhiteSpace(Id))
|
||||
throw new ArgumentNullException("Id");
|
||||
|
||||
var idSplit = UserExtensions.SplitUserId(Id);
|
||||
var slashIndex = Id.IndexOf('\\');
|
||||
|
||||
if (string.IsNullOrWhiteSpace(idSplit.Item1))
|
||||
if (slashIndex < 0)
|
||||
throw new ArgumentException(string.Format("The Id must include the Domain [{0}]", Id), "Id");
|
||||
|
||||
return TryGetDomainByNetBiosName(idSplit.Item1, out Domain);
|
||||
return TryGetDomainByNetBiosName(Id.Substring(0, slashIndex), out Domain);
|
||||
}
|
||||
public ADDomain GetDomainFromId(string Id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Id))
|
||||
throw new ArgumentNullException("Id");
|
||||
|
||||
var idSplit = UserExtensions.SplitUserId(Id);
|
||||
var slashIndex = Id.IndexOf('\\');
|
||||
|
||||
if (string.IsNullOrWhiteSpace(idSplit.Item1))
|
||||
if (slashIndex < 0)
|
||||
throw new ArgumentException(string.Format("The Id must include the Domain [{0}]", Id), "Id");
|
||||
|
||||
return GetDomainByNetBiosName(idSplit.Item1);
|
||||
return GetDomainByNetBiosName(Id.Substring(0, slashIndex));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -69,21 +69,21 @@ namespace Disco.Services.Interop.ActiveDirectory
|
||||
}
|
||||
|
||||
// Groups
|
||||
public static ADGroup AsADGroup(this ADSearchResult SearchResult)
|
||||
public static ADGroup AsADGroup(this ADSearchResult SearchResult, string[] AdditionalProperties)
|
||||
{
|
||||
return ADGroup.FromSearchResult(SearchResult);
|
||||
return ADGroup.FromSearchResult(SearchResult, AdditionalProperties);
|
||||
}
|
||||
public static IEnumerable<ADGroup> AsADGroups(this IEnumerable<ADSearchResult> SearchResults)
|
||||
public static IEnumerable<ADGroup> AsADGroups(this IEnumerable<ADSearchResult> SearchResults, string[] AdditionalProperties)
|
||||
{
|
||||
return SearchResults.Select(sr => ADGroup.FromSearchResult(sr));
|
||||
return SearchResults.Select(sr => ADGroup.FromSearchResult(sr, AdditionalProperties));
|
||||
}
|
||||
public static ADGroup AsADGroup(this ADDirectoryEntry DirectoryEntry)
|
||||
public static ADGroup AsADGroup(this ADDirectoryEntry DirectoryEntry, string[] AdditionalProperties)
|
||||
{
|
||||
return ADGroup.FromDirectoryEntry(DirectoryEntry);
|
||||
return ADGroup.FromDirectoryEntry(DirectoryEntry, AdditionalProperties);
|
||||
}
|
||||
public static IEnumerable<ADGroup> AsADGroups(this IEnumerable<ADDirectoryEntry> DirectoryEntries)
|
||||
public static IEnumerable<ADGroup> AsADGroups(this IEnumerable<ADDirectoryEntry> DirectoryEntries, string[] AdditionalProperties)
|
||||
{
|
||||
return DirectoryEntries.Select(de => ADGroup.FromDirectoryEntry(de));
|
||||
return DirectoryEntries.Select(de => ADGroup.FromDirectoryEntry(de, AdditionalProperties));
|
||||
}
|
||||
|
||||
// Organisational Units
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Disco.Data.Repository;
|
||||
|
||||
namespace Disco.Services.Interop.ActiveDirectory
|
||||
{
|
||||
using Disco.Services.Logging;
|
||||
using Disco.Services.Tasks;
|
||||
using ScheduledActionItemGrouping = List<Tuple<ADManagedGroup, List<ADManagedGroupScheduledActionItem>>>;
|
||||
|
||||
public class ActiveDirectoryManagedGroups : IDisposable
|
||||
{
|
||||
private ConcurrentDictionary<string, ADManagedGroup> managedGroups;
|
||||
private Subject<ADManagedGroupScheduledAction> actionBuffer;
|
||||
private IDisposable actionBufferSubscription;
|
||||
|
||||
internal ActiveDirectoryManagedGroups()
|
||||
{
|
||||
managedGroups = new ConcurrentDictionary<string, ADManagedGroup>();
|
||||
actionBuffer = new Subject<ADManagedGroupScheduledAction>();
|
||||
|
||||
// Subscribe, wait for no additional actions after 10 seconds
|
||||
actionBufferSubscription = actionBuffer
|
||||
.BufferWithInactivity(TimeSpan.FromSeconds(10))
|
||||
.Subscribe(ParseScheduledActions);
|
||||
}
|
||||
|
||||
#region Collection Methods
|
||||
public void AddOrUpdate(ADManagedGroup ManagedGroup)
|
||||
{
|
||||
ManagedGroup.Context = this;
|
||||
ManagedGroup.Initialize();
|
||||
|
||||
string key = ManagedGroup.Key;
|
||||
|
||||
var existingGroup = managedGroups.Values
|
||||
.Where(g => g.Key != ManagedGroup.Key)
|
||||
.FirstOrDefault(g => g.Configuration.GroupId.Equals(ManagedGroup.Configuration.GroupId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingGroup != null)
|
||||
throw new ArgumentException(string.Format("[{0}] cannot manage this group [{1}] because is already managed by [{2}]", ManagedGroup.Key, ManagedGroup.Configuration.GroupId, existingGroup.Key), "ManagedGroup");
|
||||
|
||||
managedGroups.AddOrUpdate(key, ManagedGroup, (itemKey, item) =>
|
||||
{
|
||||
item.Dispose();
|
||||
return ManagedGroup;
|
||||
});
|
||||
}
|
||||
public bool Remove(string Key)
|
||||
{
|
||||
ADManagedGroup item;
|
||||
|
||||
if (managedGroups.TryRemove(Key, out item))
|
||||
{
|
||||
item.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public bool TryGetValue(string Key, out ADManagedGroup ManagedGroup)
|
||||
{
|
||||
return managedGroups.TryGetValue(Key, out ManagedGroup);
|
||||
}
|
||||
public List<ADManagedGroup> Values
|
||||
{
|
||||
get
|
||||
{
|
||||
return managedGroups.Values.ToList();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public string ValidateGroupId(string GroupId, string IgnoreManagedGroupKey)
|
||||
{
|
||||
var group = ActiveDirectory.RetrieveADGroup(GroupId, "isCriticalSystemObject");
|
||||
if (group == null)
|
||||
throw new ArgumentException(string.Format("The group [{0}] wasn't found", GroupId), "DevicesLinkedGroup");
|
||||
if (group.GetPropertyValue<bool>("isCriticalSystemObject"))
|
||||
throw new ArgumentException(string.Format("The group [{0}] is a Critical System Active Directory Object and Disco refuses to modify it", group.DistinguishedName), "DevicesLinkedGroup");
|
||||
|
||||
GroupId = group.Id;
|
||||
|
||||
var otherManagedGroup = ActiveDirectory.Context.ManagedGroups.Values
|
||||
.Where(g => g.Key != IgnoreManagedGroupKey)
|
||||
.FirstOrDefault(g => g.Configuration.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase));
|
||||
if (otherManagedGroup != null)
|
||||
throw new ArgumentException(string.Format("Cannot manage this group [{0}] because is already managed by [{1}]", GroupId, otherManagedGroup.Key), "DevicesLinkedGroup");
|
||||
|
||||
return GroupId;
|
||||
}
|
||||
|
||||
internal void ScheduleAction(ADManagedGroupScheduledAction ScheduledAction)
|
||||
{
|
||||
actionBuffer.OnNext(ScheduledAction);
|
||||
}
|
||||
|
||||
private void ParseScheduledActions(IEnumerable<ADManagedGroupScheduledAction> Actions)
|
||||
{
|
||||
ScheduledActionItemGrouping groupedActionItems;
|
||||
|
||||
using (DiscoDataContext Database = new DiscoDataContext())
|
||||
{
|
||||
groupedActionItems = Actions
|
||||
.GroupBy(a => a.ManagedGroup)
|
||||
.Where(g =>
|
||||
{
|
||||
ADManagedGroup item;
|
||||
if (managedGroups.TryGetValue(g.Key.Key, out item))
|
||||
return item == g.Key;
|
||||
else
|
||||
return false;
|
||||
})
|
||||
.Select(g => // Reduce actions to last instance of ActionSubjectId
|
||||
Tuple.Create(
|
||||
g.Key,
|
||||
g.GroupBy(i => i.InvokingIdentifier, (id, idg) => idg.Last())
|
||||
)
|
||||
).Select(g => // Resolve action group members (action subjects)
|
||||
Tuple.Create(g.Item1, g.Item2.SelectMany(i => i.ResolveMembers(Database)))
|
||||
).Select(g => // Reduce actions to last instance of MemberId
|
||||
Tuple.Create(
|
||||
g.Item1,
|
||||
g.Item2.GroupBy(i => i.MemberId, (id, idg) => idg.Last()).ToList()
|
||||
)
|
||||
).ToList();
|
||||
}
|
||||
|
||||
ApplyScheduledActionItems(groupedActionItems);
|
||||
}
|
||||
|
||||
private void ApplyScheduledActionItems(ScheduledActionItemGrouping ActionGroups)
|
||||
{
|
||||
var actionsCount = ActionGroups.SelectMany(a => a.Item2).Count();
|
||||
if (actionsCount > 0)
|
||||
{
|
||||
var adSearchLoadProperties = new string[] { "distinguishedName", "sAMAccountName" };
|
||||
var accountDNCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (actionsCount > 40)
|
||||
{
|
||||
// Potentially over 40 accounts, cache all scoped
|
||||
var scopeAccounts = ActiveDirectory.Context.SearchScope("(|(objectCategory=computer)(objectCategory=person))", adSearchLoadProperties);
|
||||
foreach (var scopeAccount in scopeAccounts)
|
||||
{
|
||||
var id = string.Format(@"{0}\{1}", scopeAccount.Domain.NetBiosName, scopeAccount.Value<string>("sAMAccountName"));
|
||||
accountDNCache[id] = scopeAccount.Value<string>("distinguishedName");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var actionGroup in ActionGroups)
|
||||
{
|
||||
// Resolve Member Ids to AD Distinguished Names
|
||||
// Discard non-existent users
|
||||
var actionItems = actionGroup.Item2.Select(a =>
|
||||
{
|
||||
string distinguishedName;
|
||||
if (!accountDNCache.TryGetValue(a.MemberId, out distinguishedName))
|
||||
{
|
||||
string memberUsername;
|
||||
ADDomain memberDomain;
|
||||
if (!ActiveDirectory.IsValidDomainAccountId(a.MemberId, out memberUsername, out memberDomain))
|
||||
{
|
||||
accountDNCache[a.MemberId] = null; // Add to cache (avoid retries)
|
||||
return null;
|
||||
}
|
||||
|
||||
var ldapFilter = string.Format("(&(|(objectCategory=computer)(objectCategory=person))(sAMAccountName={0}))", memberUsername);
|
||||
|
||||
var adSearchResult = memberDomain.SearchEntireDomain(ldapFilter, adSearchLoadProperties, ActiveDirectory.SingleSearchResult).FirstOrDefault();
|
||||
if (adSearchResult != null)
|
||||
{
|
||||
var adSearchResultDN = adSearchResult.Value<string>("distinguishedName");
|
||||
accountDNCache[a.MemberId] = adSearchResultDN; // Add to cache
|
||||
a.MemberDistinguishedName = adSearchResultDN; // Update ActionItem
|
||||
return a;
|
||||
}
|
||||
else
|
||||
{
|
||||
accountDNCache[a.MemberId] = null; // Add to cache (avoid retries)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (distinguishedName == null)
|
||||
return null;
|
||||
else
|
||||
{
|
||||
a.MemberDistinguishedName = distinguishedName; // Update ActionItem
|
||||
return a;
|
||||
}
|
||||
}).Where(a => a != null).ToList();
|
||||
|
||||
if (actionItems.Count > 0)
|
||||
{
|
||||
var adGroup = actionGroup.Item1.GetGroup();
|
||||
if (adGroup == null)
|
||||
{
|
||||
SystemLog.LogWarning("Active Directory Managed Group", actionGroup.Item1.Key, "Group Not Found", actionGroup.Item1.Configuration.GroupId);
|
||||
break;
|
||||
}
|
||||
var adGroupMembers = adGroup.GetPropertyValues<string>("member").ToList();
|
||||
actionItems = actionItems.Where(a =>
|
||||
{
|
||||
switch (a.ActionType)
|
||||
{
|
||||
case ADManagedGroupScheduledActionType.AddGroupMember:
|
||||
return !adGroupMembers.Contains(a.MemberDistinguishedName);
|
||||
case ADManagedGroupScheduledActionType.RemoveGroupMember:
|
||||
return adGroupMembers.Contains(a.MemberDistinguishedName);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
if (actionItems.Count > 0)
|
||||
{
|
||||
using (var adGroupEntry = ActiveDirectory.Context.RetrieveDirectoryEntry(adGroup.DistinguishedName, new string[] { "member", "isCriticalSystemObject" }))
|
||||
{
|
||||
if (adGroupEntry.Entry.Properties.Value<bool>("isCriticalSystemObject"))
|
||||
throw new InvalidOperationException(string.Format("This group [{0}] is a Critical System Active Directory Object and Disco refuses to modify it", adGroup.DistinguishedName));
|
||||
|
||||
var adGroupEntryMembers = adGroupEntry.Entry.Properties["member"];
|
||||
foreach (var item in actionItems)
|
||||
{
|
||||
switch (item.ActionType)
|
||||
{
|
||||
case ADManagedGroupScheduledActionType.AddGroupMember:
|
||||
if (!adGroupEntryMembers.Contains(item.MemberDistinguishedName))
|
||||
{
|
||||
// Add Member Entry
|
||||
adGroupEntryMembers.Add(item.MemberDistinguishedName);
|
||||
}
|
||||
break;
|
||||
case ADManagedGroupScheduledActionType.RemoveGroupMember:
|
||||
if (adGroupEntryMembers.Contains(item.MemberDistinguishedName))
|
||||
{
|
||||
// Add Member Entry
|
||||
adGroupEntryMembers.Remove(item.MemberDistinguishedName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Commit Changes
|
||||
adGroupEntry.Entry.CommitChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int SyncManagedGroups(IScheduledTaskStatus Status)
|
||||
{
|
||||
return SyncManagedGroups(managedGroups.Values, Status);
|
||||
}
|
||||
|
||||
public int SyncManagedGroups(ADManagedGroup ManagedGroup, IScheduledTaskStatus Status)
|
||||
{
|
||||
return SyncManagedGroups(new ADManagedGroup[] { ManagedGroup }, Status);
|
||||
}
|
||||
|
||||
public int SyncManagedGroups(IEnumerable<ADManagedGroup> ManagedGroups, IScheduledTaskStatus Status)
|
||||
{
|
||||
List<ADManagedGroup> managedGroups = ManagedGroups.ToList();
|
||||
ScheduledActionItemGrouping actionGroups;
|
||||
int changeCount = 0;
|
||||
|
||||
Status.UpdateStatus(0, "Determining Managed Group Members");
|
||||
|
||||
using (DiscoDataContext Database = new DiscoDataContext())
|
||||
{
|
||||
actionGroups = managedGroups.Select((g, index) =>
|
||||
{
|
||||
Status.UpdateStatus(
|
||||
((double)30 / managedGroups.Count) * index, // 0 -> 30
|
||||
string.Format("Determining Group Members: {0} [{1}]", g.GroupDescription, g.Configuration.GroupId));
|
||||
return Tuple.Create(
|
||||
g,
|
||||
g.DetermineMembers(Database).Select(m =>
|
||||
new ADManagedGroupScheduledActionItem(
|
||||
g,
|
||||
ADManagedGroupScheduledActionType.AddGroupMember,
|
||||
m
|
||||
)).ToList());
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
var actionsCount = actionGroups.SelectMany(a => a.Item2).Count();
|
||||
if (actionsCount > 0)
|
||||
{
|
||||
Status.UpdateStatus(30, "Resolving Group Members");
|
||||
|
||||
var adSearchLoadProperties = new string[] { "distinguishedName", "sAMAccountName", "displayName", "name" };
|
||||
var accountDNCache = new Dictionary<string, Tuple<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||
if (actionsCount > 40)
|
||||
{
|
||||
// Potentially over 40 accounts, cache all scoped
|
||||
var scopeAccounts = ActiveDirectory.Context.SearchScope("(|(objectCategory=computer)(objectCategory=person))", adSearchLoadProperties);
|
||||
foreach (var scopeAccount in scopeAccounts)
|
||||
{
|
||||
var id = string.Format(@"{0}\{1}", scopeAccount.Domain.NetBiosName, scopeAccount.Value<string>("sAMAccountName"));
|
||||
accountDNCache[id] = Tuple.Create(scopeAccount.Value<string>("distinguishedName"), scopeAccount.Value<string>("displayName") ?? scopeAccount.Value<string>("name"));
|
||||
}
|
||||
}
|
||||
|
||||
actionGroups = actionGroups.Select((g, index) =>
|
||||
{
|
||||
Status.UpdateStatus(
|
||||
30 + (((double)30 / actionGroups.Count) * index), // 30 -> 60
|
||||
string.Format("Resolving {0} Group Members: {1} [{2}]", g.Item2.Count, g.Item1.GroupDescription, g.Item1.Configuration.GroupId));
|
||||
|
||||
// Resolve Member Ids to AD Distinguished Names
|
||||
// Discard non-existent users
|
||||
return Tuple.Create(
|
||||
g.Item1,
|
||||
g.Item2.Select(a =>
|
||||
{
|
||||
Tuple<string, string> definition;
|
||||
if (!accountDNCache.TryGetValue(a.MemberId, out definition))
|
||||
{
|
||||
string memberUsername;
|
||||
ADDomain memberDomain;
|
||||
if (!ActiveDirectory.IsValidDomainAccountId(a.MemberId, out memberUsername, out memberDomain))
|
||||
{
|
||||
accountDNCache[a.MemberId] = null; // Add to cache (avoid retries)
|
||||
return null;
|
||||
}
|
||||
|
||||
var ldapFilter = string.Format("(&(|(objectCategory=computer)(objectCategory=person))(sAMAccountName={0}))", memberUsername);
|
||||
|
||||
var adSearchResult = memberDomain.SearchEntireDomain(ldapFilter, adSearchLoadProperties, ActiveDirectory.SingleSearchResult).FirstOrDefault();
|
||||
if (adSearchResult != null)
|
||||
{
|
||||
definition = Tuple.Create(adSearchResult.Value<string>("distinguishedName"), adSearchResult.Value<string>("displayName") ?? adSearchResult.Value<string>("name"));
|
||||
accountDNCache[a.MemberId] = definition; // Add to cache
|
||||
}
|
||||
else
|
||||
{
|
||||
accountDNCache[a.MemberId] = null; // Add to cache (avoid retries)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (definition == null)
|
||||
return null;
|
||||
|
||||
a.MemberDistinguishedName = definition.Item1; // Update ActionItem
|
||||
a.MemberDisplayName = definition.Item2;
|
||||
return a;
|
||||
}).Where(a => a != null).ToList());
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
foreach (var actionGroup in actionGroups)
|
||||
{
|
||||
var adGroup = actionGroup.Item1.GetGroup();
|
||||
if (adGroup == null)
|
||||
{
|
||||
SystemLog.LogWarning("Active Directory Managed Group", actionGroup.Item1.Key, "Group Not Found", actionGroup.Item1.Configuration.GroupId);
|
||||
break;
|
||||
}
|
||||
|
||||
Status.UpdateStatus(
|
||||
60 + (((double)40 / actionGroups.Count) * actionGroups.IndexOf(actionGroup)), // 60 -> 100
|
||||
string.Format("Synchronizing {0} Group Members: {1} [{2}]", actionGroup.Item2.Count, actionGroup.Item1.GroupDescription, actionGroup.Item1.Configuration.GroupId));
|
||||
|
||||
using (var adGroupEntry = ActiveDirectory.Context.RetrieveDirectoryEntry(adGroup.DistinguishedName, new string[] { "isCriticalSystemObject", "description", "member" }))
|
||||
{
|
||||
if (adGroupEntry.Entry.Properties.Value<bool>("isCriticalSystemObject"))
|
||||
throw new InvalidOperationException(string.Format("This group [{0}] is a Critical System Active Directory Object and Disco refuses to modify it", adGroup.DistinguishedName));
|
||||
|
||||
// Update Description
|
||||
var groupDescription = string.Format("Disco ICT: {0}", actionGroup.Item1.GroupDescription);
|
||||
if (adGroupEntry.Entry.Properties.Value<string>("description") != groupDescription)
|
||||
{
|
||||
var adGroupEntryDescription = adGroupEntry.Entry.Properties["description"];
|
||||
if (adGroupEntryDescription.Count > 0)
|
||||
adGroupEntryDescription.Clear();
|
||||
adGroupEntryDescription.Add(groupDescription);
|
||||
}
|
||||
|
||||
// Sync Members
|
||||
var adGroupEntryMembers = adGroupEntry.Entry.Properties["member"];
|
||||
|
||||
// Remove Items
|
||||
var removeItems = adGroupEntryMembers
|
||||
.Cast<string>()
|
||||
.Except(actionGroup.Item2.Select(i => i.MemberDistinguishedName))
|
||||
.ToList();
|
||||
removeItems.ForEach(i => adGroupEntryMembers.Remove(i));
|
||||
|
||||
// Add Items
|
||||
var addItems = actionGroup.
|
||||
Item2.Select(i => i.MemberDistinguishedName)
|
||||
.Except(adGroupEntryMembers.Cast<string>())
|
||||
.ToList();
|
||||
addItems.ForEach(i => adGroupEntryMembers.Add(i));
|
||||
|
||||
// Commit Changes
|
||||
adGroupEntry.Entry.CommitChanges();
|
||||
|
||||
changeCount += removeItems.Count;
|
||||
changeCount += addItems.Count;
|
||||
}
|
||||
}
|
||||
|
||||
Status.UpdateStatus(100, "Managed Group Synchronization Finished");
|
||||
|
||||
return changeCount;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (actionBufferSubscription != null)
|
||||
actionBufferSubscription.Dispose();
|
||||
|
||||
if (actionBuffer != null)
|
||||
actionBuffer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal class ADManagedGroupScheduledAction
|
||||
{
|
||||
private Func<DiscoDataContext, IEnumerable<string>> memberResolver;
|
||||
|
||||
public ADManagedGroup ManagedGroup { get; private set; }
|
||||
public ADManagedGroupScheduledActionType ActionType { get; private set; }
|
||||
public string InvokingIdentifier { get; set; }
|
||||
|
||||
public ADManagedGroupScheduledAction(ADManagedGroup ManagedGroup, ADManagedGroupScheduledActionType ActionType, string InvokingIdentifier, Func<DiscoDataContext, IEnumerable<string>> MemberResolver)
|
||||
{
|
||||
this.ManagedGroup = ManagedGroup;
|
||||
this.ActionType = ActionType;
|
||||
this.InvokingIdentifier = InvokingIdentifier;
|
||||
this.memberResolver = MemberResolver;
|
||||
}
|
||||
|
||||
public IEnumerable<ADManagedGroupScheduledActionItem> ResolveMembers(DiscoDataContext Database)
|
||||
{
|
||||
if (memberResolver != null)
|
||||
{
|
||||
var members = memberResolver(Database);
|
||||
if (members == null)
|
||||
return Enumerable.Empty<ADManagedGroupScheduledActionItem>();
|
||||
else
|
||||
return members.Select(m =>
|
||||
new ADManagedGroupScheduledActionItem(this.ManagedGroup, this.ActionType, m)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ADManagedGroupScheduledActionItem[]
|
||||
{
|
||||
new ADManagedGroupScheduledActionItem(this.ManagedGroup, this.ActionType, this.InvokingIdentifier)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
internal class ADManagedGroupScheduledActionItem
|
||||
{
|
||||
public ADManagedGroup ManagedGroup { get; private set; }
|
||||
public ADManagedGroupScheduledActionType ActionType { get; private set; }
|
||||
public string MemberId { get; set; }
|
||||
public string MemberDistinguishedName { get; set; }
|
||||
public string MemberDisplayName { get; set; }
|
||||
|
||||
public ADManagedGroupScheduledActionItem(ADManagedGroup ManagedGroup, ADManagedGroupScheduledActionType ActionType, string MemberId)
|
||||
{
|
||||
this.ManagedGroup = ManagedGroup;
|
||||
this.ActionType = ActionType;
|
||||
this.MemberId = MemberId;
|
||||
}
|
||||
}
|
||||
internal enum ADManagedGroupScheduledActionType
|
||||
{
|
||||
AddGroupMember,
|
||||
RemoveGroupMember
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user