From 090322126a5289f1af4cccdccaf17ae5b1699247 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Tue, 3 Sep 2013 20:21:20 +1000 Subject: [PATCH] Fix #16: Include Primary Group for AD Users Caches group SIDs and retires unmanaged SID transform PInvoke for managed equivalents --- .../ActiveDirectory/ActiveDirectory.cs | 15 +- .../ActiveDirectoryCachedGroups.cs | 147 ++++++++++++++++-- .../ActiveDirectory/ActiveDirectoryHelpers.cs | 51 ++++-- 3 files changed, 185 insertions(+), 28 deletions(-) diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectory.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectory.cs index 458ed31b..f38a584c 100644 --- a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectory.cs +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectory.cs @@ -69,7 +69,7 @@ namespace Disco.BI.Interop.ActiveDirectory string name = result.Properties["name"][0].ToString(); string sAMAccountName = result.Properties["sAMAccountName"][0].ToString(); string distinguishedName = result.Properties["distinguishedName"][0].ToString(); - string objectSid = ActiveDirectoryHelpers.ConvertBytesToSIDString((byte[])result.Properties["objectSid"][0]); + string objectSid = ActiveDirectoryHelpers.ConvertBytesToSDDLString((byte[])result.Properties["objectSid"][0]); var dNSNameProperty = result.Properties["dNSHostName"]; string dNSName = null; @@ -117,7 +117,8 @@ namespace Disco.BI.Interop.ActiveDirectory string name = result.Properties["name"][0].ToString(); string username = result.Properties["sAMAccountName"][0].ToString(); string distinguishedName = result.Properties["distinguishedName"][0].ToString(); - string objectSid = ActiveDirectoryHelpers.ConvertBytesToSIDString((byte[])result.Properties["objectSid"][0]); + byte[] objectSid = (byte[])result.Properties["objectSid"][0]; + string objectSidSDDL = ActiveDirectoryHelpers.ConvertBytesToSDDLString(objectSid); ResultPropertyValueCollection displayNameProp = result.Properties["displayName"]; string displayName = username; @@ -140,7 +141,10 @@ namespace Disco.BI.Interop.ActiveDirectory if (phoneProp.Count > 0) phone = phoneProp[0].ToString(); - IEnumerable groupCNs = result.Properties["memberOf"].Cast(); + int primaryGroupID = (int)result.Properties["primaryGroupID"][0]; + string primaryGroupSid = ActiveDirectoryHelpers.ConvertBytesToSDDLString(ActiveDirectoryHelpers.BuildPrimaryGroupSid(objectSid, primaryGroupID)); + var groupCNs = result.Properties["memberOf"].Cast().ToList(); + groupCNs.Add(ActiveDirectoryCachedGroups.GetGroupsCnForSid(primaryGroupSid)); List groups = ActiveDirectoryCachedGroups.GetGroups(groupCNs).Select(g => g.ToLower()).ToList(); //foreach (string groupCN in result.Properties["memberOf"]) @@ -194,7 +198,7 @@ namespace Disco.BI.Interop.ActiveDirectory DistinguishedName = distinguishedName, sAMAccountName = username, DisplayName = displayName, - ObjectSid = objectSid, + ObjectSid = objectSidSDDL, Type = type, Path = result.Path, LoadedProperties = additionalProperties @@ -219,7 +223,8 @@ namespace Disco.BI.Interop.ActiveDirectory "sn", "givenName", "memberOf", - "mail", + "primaryGroupID", + "mail", "telephoneNumber" }; loadProperties.AddRange(AdditionalProperties); diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryCachedGroups.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryCachedGroups.cs index cffee7f7..e1e2892b 100644 --- a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryCachedGroups.cs +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryCachedGroups.cs @@ -13,6 +13,7 @@ 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 @@ -33,6 +34,14 @@ namespace Disco.BI.Interop.ActiveDirectory 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); @@ -62,8 +71,27 @@ namespace Disco.BI.Interop.ActiveDirectory if (groupRecord == null) { // Load from AD - var group = ADCachedGroup.LoadFromAD(GroupCN); - SetValue(GroupCN, group); + 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; } @@ -83,33 +111,70 @@ namespace Disco.BI.Interop.ActiveDirectory if (groupRecord.Item2 > DateTime.Now) return groupRecord; else - _Cache.TryRemove(groupCN, out groupRecord); + { + if (_Cache.TryRemove(groupCN, out groupRecord)) + _SidCache.TryRemove(groupRecord.Item1.ObjectSid, out groupRecord); + } } return null; } - private static bool SetValue(string GroupCN, ADCachedGroup GroupRecord) + private static Tuple TrySidCache(string GroupSid) { - string key = GroupCN.ToLower(); - Tuple groupRecord = new Tuple(GroupRecord, DateTime.Now.AddTicks(CacheTimeoutTicks)); - if (_Cache.ContainsKey(key)) + Tuple groupRecord; + if (_SidCache.TryGetValue(GroupSid, out groupRecord)) { - Tuple oldGroupRecord; - if (_Cache.TryGetValue(key, out oldGroupRecord)) + if (groupRecord.Item2 > DateTime.Now) + return groupRecord; + else { - return _Cache.TryUpdate(key, groupRecord, oldGroupRecord); + if (_SidCache.TryRemove(GroupSid, out groupRecord)) + _Cache.TryRemove(groupRecord.Item1.CN.ToLower(), out groupRecord); } } - return _Cache.TryAdd(key, 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 LoadFromAD(string CN) + public static ADCachedGroup LoadWithCN(string CN) { ADCachedGroup group = null; @@ -122,6 +187,7 @@ namespace Disco.BI.Interop.ActiveDirectory CN = CN }; + group.ObjectSid = ActiveDirectoryHelpers.ConvertBytesToSDDLString((byte[])groupDE.Properties["objectSid"].Value); group.FriendlyName = (string)groupDE.Properties["sAMAccountName"].Value; var groupMemberOf = groupDE.Properties["memberOf"]; @@ -135,6 +201,46 @@ namespace Disco.BI.Interop.ActiveDirectory 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 @@ -143,6 +249,7 @@ namespace Disco.BI.Interop.ActiveDirectory private static void CleanStaleCache() { + // Clean Cache var groupKeys = _Cache.Keys.ToArray(); foreach (string groupKey in groupKeys) { @@ -150,7 +257,23 @@ namespace Disco.BI.Interop.ActiveDirectory 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); + } } } } diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryHelpers.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryHelpers.cs index ad077645..cbf08808 100644 --- a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryHelpers.cs +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryHelpers.cs @@ -5,6 +5,7 @@ using System.DirectoryServices; using System.DirectoryServices.ActiveDirectory; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Security.Principal; using System.Text; using System.Threading; @@ -128,22 +129,50 @@ namespace Disco.BI.Interop.ActiveDirectory } #endregion - [System.Runtime.InteropServices.DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern bool ConvertSidToStringSid(byte[] pSID, ref System.Text.StringBuilder ptrSid); - internal static string ConvertBytesToSIDString(byte[] SID) + internal static string ConvertBytesToSDDLString(byte[] SID) { - System.Text.StringBuilder sidString = new System.Text.StringBuilder(); - bool flag = ActiveDirectoryHelpers.ConvertSidToStringSid(SID, ref sidString); - string ConvertBytesToSIDString; - if (flag) + SecurityIdentifier sID = new SecurityIdentifier(SID, 0); + + return sID.ToString(); + } + + internal static byte[] ConvertSDDLStringToBytes(string SidSsdlString) + { + SecurityIdentifier sID = new SecurityIdentifier(SidSsdlString); + + var sidBytes = new byte[sID.BinaryLength]; + + sID.GetBinaryForm(sidBytes, 0); + + return sidBytes; + } + + internal static byte[] BuildPrimaryGroupSid(byte[] UserSID, int PrimaryGroupId) + { + var groupSid = (byte[])UserSID.Clone(); + + int ridOffset = groupSid.Length - 4; + int groupId = PrimaryGroupId; + for (int i = 0; i < 4; i++) { - ConvertBytesToSIDString = sidString.ToString(); + groupSid[ridOffset + i] = (byte)(groupId & 0xFF); + groupId >>= 8; } - else + + return groupSid; + } + + internal static string ConvertBytesToBinarySidString(byte[] SID) + { + StringBuilder escapedSid = new StringBuilder(); + + foreach (var sidByte in SID) { - ConvertBytesToSIDString = null; + escapedSid.Append('\\'); + escapedSid.Append(sidByte.ToString("x2")); } - return ConvertBytesToSIDString; + + return escapedSid.ToString(); } internal static string EscapeLdapQuery(string query)