-
Notifications
You must be signed in to change notification settings - Fork 56
fix: Make ACLProcessor guid cache singleton and thread-safe - BED-9236 #309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,15 +12,83 @@ | |
| using SharpHoundCommonLib.Enums; | ||
| using SharpHoundCommonLib.OutputTypes; | ||
| using System.Linq; | ||
| using System.Threading; | ||
|
|
||
| namespace SharpHoundCommonLib.Processors { | ||
| /// <summary> | ||
| /// Owns state shared by processor instances and gives that state an explicit lifetime. | ||
| /// </summary> | ||
| public sealed class ACLProcessorContext : IDisposable { | ||
| private readonly ACLProcessor.GuidCache _aclGuidCache = new(); | ||
| private int _disposed; | ||
|
|
||
| /// <summary> | ||
| /// Creates an <see cref="ACLProcessor"/> that shares its GUID cache with other | ||
| /// ACL processors created by this context. | ||
| /// </summary> | ||
| public ACLProcessor CreateACLProcessor(ILdapUtils utils, ILogger log = null) { | ||
| if (Volatile.Read(ref _disposed) != 0) { | ||
| throw new ObjectDisposedException(nameof(ACLProcessorContext)); | ||
| } | ||
|
|
||
| return new ACLProcessor(utils, _aclGuidCache, log); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Clears the shared processor state. Processors created by this context must not | ||
| /// be used after the context is disposed. | ||
| /// </summary> | ||
| public void Dispose() { | ||
| if (Interlocked.Exchange(ref _disposed, 1) != 0) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used this pattern once with adaptive timeout stuff. |
||
| return; | ||
| } | ||
|
|
||
| _aclGuidCache.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| public class ACLProcessor { | ||
| private static readonly Dictionary<Label, string> BaseGuids; | ||
| private readonly ConcurrentDictionary<string, string> _guidMap = new(); | ||
| private readonly ILogger _log; | ||
| private readonly ILdapUtils _utils; | ||
| private readonly ConcurrentHashSet _builtDomainCaches = new(StringComparer.OrdinalIgnoreCase); | ||
| private readonly object _lock = new(); | ||
| private readonly GuidCache _guidCache; | ||
|
|
||
| internal sealed class GuidCache : IDisposable { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kept in here so it can be type-referenced via |
||
| private readonly ConcurrentDictionary<string, string> _guidMap = new(); | ||
| private readonly ConcurrentDictionary<string, Lazy<Task>> _buildTasks = | ||
| new(StringComparer.OrdinalIgnoreCase); | ||
| private int _disposed; | ||
|
|
||
| public Lazy<Task> GetOrAddBuildTask(string domain, Func<Lazy<Task>> buildTaskFactory) { | ||
| ThrowIfDisposed(); | ||
| return _buildTasks.GetOrAdd(domain, _ => buildTaskFactory()); | ||
| } | ||
|
|
||
| public void AddGuid(string guid, string name) { | ||
| ThrowIfDisposed(); | ||
| _guidMap.TryAdd(guid, name); | ||
| } | ||
|
|
||
| public bool TryGetGuid(string guid, out string name) { | ||
| ThrowIfDisposed(); | ||
| return _guidMap.TryGetValue(guid, out name); | ||
|
Comment on lines
+57
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Scope GUID mappings by domain. Line 57 stores LAPS mappings by GUID only, while build tasks are scoped by domain. If one Key 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| public void Dispose() { | ||
| if (Interlocked.Exchange(ref _disposed, 1) != 0) { | ||
| return; | ||
| } | ||
|
|
||
| _buildTasks.Clear(); | ||
| _guidMap.Clear(); | ||
| } | ||
|
|
||
| private void ThrowIfDisposed() { | ||
| if (Volatile.Read(ref _disposed) != 0) { | ||
| throw new ObjectDisposedException(nameof(ACLProcessorContext)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| static ACLProcessor() { | ||
| //Create a dictionary with the base GUIDs of each object type | ||
|
|
@@ -42,9 +110,12 @@ static ACLProcessor() { | |
| }; | ||
| } | ||
|
|
||
| public ACLProcessor(ILdapUtils utils, ILogger log = null) | ||
| { | ||
| public ACLProcessor(ILdapUtils utils, ILogger log = null) : this(utils, new GuidCache(), log) { | ||
| } | ||
|
|
||
| internal ACLProcessor(ILdapUtils utils, GuidCache guidCache, ILogger log = null) { | ||
| _utils = utils; | ||
| _guidCache = guidCache; | ||
| _log = log ?? Logging.LogProvider.CreateLogger("ACLProc"); | ||
| } | ||
|
|
||
|
|
@@ -73,14 +144,14 @@ public override string ToString() { | |
| /// LAPS | ||
| /// </summary> | ||
| private async Task BuildGuidCache(string domain) { | ||
| lock (_lock) { | ||
| if (_builtDomainCaches.Contains(domain)) { | ||
| return; | ||
| } | ||
| var buildTask = _guidCache.GetOrAddBuildTask(domain, | ||
| // The ExecutionAndPublication mode ensures that only one thread can execute the factory method at a time, and all other threads will wait for the result of that execution. This prevents multiple threads from building the cache simultaneously for the same domain. | ||
| () => new Lazy<Task>(() => BuildGuidCacheCore(domain), LazyThreadSafetyMode.ExecutionAndPublication)); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's what actually fixes the BED-9236 bug. |
||
|
|
||
| _builtDomainCaches.Add(domain); | ||
| } | ||
| await buildTask.Value; | ||
| } | ||
|
|
||
| private async Task BuildGuidCacheCore(string domain) { | ||
| _log.LogInformation("Building GUID Cache for {Domain}", domain); | ||
| await foreach (var result in _utils.PagedQuery(new LdapQueryParameters { | ||
| DomainName = domain, | ||
|
|
@@ -108,7 +179,7 @@ private async Task BuildGuidCache(string domain) { | |
|
|
||
| if (name is LDAPProperties.LAPSPlaintextPassword or LDAPProperties.LAPSEncryptedPassword or LDAPProperties.LegacyLAPSPassword) { | ||
| _log.LogInformation("Found GUID for ACL Right {Name}: {Guid} in domain {Domain}", name, guid, domain); | ||
| _guidMap.TryAdd(guid, name); | ||
| _guidCache.AddGuid(guid, name); | ||
| } | ||
| } else { | ||
| _log.LogDebug("Error while building GUID cache for {Domain}: {Message}", domain, result.Error); | ||
|
|
@@ -676,7 +747,7 @@ public async IAsyncEnumerable<ACE> ProcessACL(byte[] ntSecurityDescriptor, strin | |
| IsPermissionForOwnerRightsSid = isPermissionForOwnerRightsSid, | ||
| IsInheritedPermissionForOwnerRightsSid = isInheritedPermissionForOwnerRightsSid, | ||
| }; | ||
| else if (_guidMap.TryGetValue(aceType, out var lapsAttribute)) { | ||
| else if (_guidCache.TryGetGuid(aceType, out var lapsAttribute)) { | ||
| // Compare the retrieved attribute name against LDAPProperties values | ||
| if (lapsAttribute == LDAPProperties.LegacyLAPSPassword || | ||
| lapsAttribute == LDAPProperties.LAPSPlaintextPassword || | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Volatile.Readis technically more thread-safe than a direct read. Unlikely to be necessary, but better safe than sorry.