using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.DirectoryServices; using System.Linq; using System.Text; using System.Threading.Tasks; using Disco.Data.Repository; using Disco.Services.Tasks; using Quartz; namespace Disco.BI.Interop.ActiveDirectory { public class ActiveDirectoryCachedGroups : ScheduledTask { private static ConcurrentDictionary> _SidCache = new ConcurrentDictionary>(); private static ConcurrentDictionary> _Cache = new ConcurrentDictionary>(); private const long CacheTimeoutTicks = 6000000000; // 10 Minutes public static IEnumerable GetGroups(IEnumerable GroupCNs) { List groups = new List(); foreach (var groupCN in GroupCNs) foreach (var group in GetGroupsRecursive(groupCN, new Stack())) if (!groups.Contains(group)) { groups.Add(group); yield return group.FriendlyName; } } public static IEnumerable GetGroups(string GroupCN) { foreach (var group in GetGroupsRecursive(GroupCN, new Stack())) yield return group.FriendlyName; } public static string GetGroupsCnForSid(string GroupSid) { var sidGroup = GetGroupBySid(GroupSid); if (sidGroup == null) return null; else return sidGroup.CN; } private static IEnumerable GetGroupsRecursive(string GroupCN, Stack RecursiveTree) { var group = GetGroup(GroupCN); if (group != null && !RecursiveTree.Contains(group)) { yield return group; if (group.MemberOf != null) { RecursiveTree.Push(group); foreach (var memberOfGroupCN in group.MemberOf) foreach (var memberOfGroup in GetGroupsRecursive(memberOfGroupCN, RecursiveTree)) yield return memberOfGroup; RecursiveTree.Pop(); } } } private static ADCachedGroup GetGroup(string GroupCN) { // Check Cache Tuple groupRecord = TryCache(GroupCN); if (groupRecord == null) { // Load from AD var group = ADCachedGroup.LoadWithCN(GroupCN); SetValue(group); return group; } else { // Return from Cache return groupRecord.Item1; } } private static ADCachedGroup GetGroupBySid(string GroupSid) { // Check Cache Tuple groupRecord = TrySidCache(GroupSid); if (groupRecord == null) { // Load from AD var group = ADCachedGroup.LoadWithSid(GroupSid); SetValue(group); return group; } else { // Return from Cache return groupRecord.Item1; } } private static Tuple TryCache(string GroupCN) { string groupCN = GroupCN.ToLower(); Tuple groupRecord; if (_Cache.TryGetValue(groupCN, out groupRecord)) { if (groupRecord.Item2 > DateTime.Now) return groupRecord; else { if (_Cache.TryRemove(groupCN, out groupRecord)) _SidCache.TryRemove(groupRecord.Item1.ObjectSid, out groupRecord); } } return null; } private static Tuple TrySidCache(string GroupSid) { Tuple groupRecord; if (_SidCache.TryGetValue(GroupSid, out groupRecord)) { if (groupRecord.Item2 > DateTime.Now) return groupRecord; else { if (_SidCache.TryRemove(GroupSid, out groupRecord)) _Cache.TryRemove(groupRecord.Item1.CN.ToLower(), out groupRecord); } } return null; } private static bool SetValue(ADCachedGroup GroupRecord) { Tuple groupRecord = new Tuple(GroupRecord, DateTime.Now.AddTicks(CacheTimeoutTicks)); Tuple oldGroupRecord; string key = GroupRecord.CN.ToLower(); if (_Cache.ContainsKey(key)) { if (_Cache.TryGetValue(key, out oldGroupRecord)) { _Cache.TryUpdate(key, groupRecord, oldGroupRecord); } } else { _Cache.TryAdd(key, groupRecord); } string sid = GroupRecord.ObjectSid; if (_SidCache.ContainsKey(sid)) { if (_SidCache.TryGetValue(sid, out oldGroupRecord)) { _SidCache.TryUpdate(sid, groupRecord, oldGroupRecord); } } else { _SidCache.TryAdd(sid, groupRecord); } return true; } private class ADCachedGroup { public string ObjectSid { get; set; } public string CN { get; private set; } public string FriendlyName { get; private set; } public List MemberOf { get; private set; } public static ADCachedGroup LoadWithCN(string CN) { ADCachedGroup group = null; using (DirectoryEntry groupDE = new DirectoryEntry(string.Concat(ActiveDirectoryHelpers.DefaultLdapPath, CN))) { if (groupDE != null) { group = new ADCachedGroup() { CN = CN }; group.ObjectSid = ActiveDirectoryHelpers.ConvertBytesToSDDLString((byte[])groupDE.Properties["objectSid"].Value); group.FriendlyName = (string)groupDE.Properties["sAMAccountName"].Value; var groupMemberOf = groupDE.Properties["memberOf"]; if (groupMemberOf != null && groupMemberOf.Count > 0) { group.MemberOf = groupMemberOf.Cast().ToList(); } } } return group; } public static ADCachedGroup LoadWithSid(string Sid) { using (DirectoryEntry dRootEntry = ActiveDirectoryHelpers.DefaultLdapRoot) { var loadProperties = new List { "distinguishedName", "objectSid", "sAMAccountName", "memberOf" }; var sidBytes = ActiveDirectoryHelpers.ConvertSDDLStringToBytes(Sid); var sidBinaryString = ActiveDirectoryHelpers.ConvertBytesToBinarySidString(sidBytes); using (DirectorySearcher dSearcher = new DirectorySearcher(dRootEntry, string.Format("(&(objectClass=group)(objectSid={0}))", sidBinaryString), loadProperties.ToArray(), SearchScope.Subtree)) { SearchResult dResult = dSearcher.FindOne(); if (dResult != null) { var group = new ADCachedGroup() { CN = (string)dResult.Properties["distinguishedName"][0], ObjectSid = ActiveDirectoryHelpers.ConvertBytesToSDDLString((byte[])dResult.Properties["objectSid"][0]), FriendlyName = (string)dResult.Properties["sAMAccountName"][0] }; var groupMemberOf = dResult.Properties["memberOf"]; if (groupMemberOf != null && groupMemberOf.Count > 0) { group.MemberOf = groupMemberOf.Cast().ToList(); } return group; } else return null; } } } private ADCachedGroup() { // Private Constructor } } private static void CleanStaleCache() { // Clean Cache var groupKeys = _Cache.Keys.ToArray(); foreach (string groupKey in groupKeys) { Tuple groupRecord; if (_Cache.TryGetValue(groupKey, out groupRecord)) { if (groupRecord.Item2 <= DateTime.Now) { _Cache.TryRemove(groupKey, out groupRecord); } } } // Clean SID Cache groupKeys = _SidCache.Keys.ToArray(); foreach (string groupKey in groupKeys) { Tuple groupRecord; if (_SidCache.TryGetValue(groupKey, out groupRecord)) { if (groupRecord.Item2 <= DateTime.Now) { _SidCache.TryRemove(groupKey, out groupRecord); } } } } public override string TaskName { get { return "AD Group Cache - Clean Stale Cache"; } } public override bool SingleInstanceTask { get { return true; } } public override bool CancelInitiallySupported { get { return false; } } public override bool LogExceptionsOnly { get { return true; } } public override void InitalizeScheduledTask(DiscoDataContext dbContext) { // Run @ every 15mins // Next 15min interval DateTime now = DateTime.Now; int mins = (15 - (now.Minute % 15)); if (mins < 10) mins += 15; DateTimeOffset startAt = new DateTimeOffset(now).AddMinutes(mins).AddSeconds(now.Second * -1).AddMilliseconds(now.Millisecond * -1); TriggerBuilder triggerBuilder = TriggerBuilder.Create().StartAt(startAt). WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(15)); this.ScheduleTask(triggerBuilder); } protected override void ExecuteTask() { CleanStaleCache(); } } }