diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-10 02:26:09 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-10 02:26:09 +0900 |
| commit | f2001b0c5a0b35e052ba62ef4f2e19bcc64fd870 (patch) | |
| tree | d9ed012a2a2dfacc24e5b7e765b3919a2ae6033f | |
| parent | 60d558e9a7eb1944fc58ee88e121a5db5248a332 (diff) | |
perf(sandbox): apply ACLs at approved roots
Replace per-descendant ACL enumeration, security descriptor snapshots, journal entries, audit events, and cleanup rewrites with one inheritable ACE and one snapshot per approved directory root. Exact file approvals continue to receive non-inheritable file ACEs, and nested roots retain independent least-privilege scopes cleaned in reverse order.
Keep protected descendant DACLs protected instead of disabling inheritance protection. Such paths remain OS-denied, preserving fail-safe behavior without silently expanding the SandboxPolicy.
Remove the obsolete descendant normalization pass. Restoring the root security descriptor removes inherited Application SID ACEs from existing and Task-created descendants; crash recovery performs the same root descriptor restoration from the journal.
Add tests for existing-child inheritance, newly-created-child inheritance, protected descendants, one journal entry per root, exact descriptor restoration, no residual Application SID ACE, and independent-process crash recovery. Extend the existing elevated recovery and backend tests to validate inherited child cleanup and one ACL apply/restore event per root.
Verification: Release build passed with 0 warnings and 0 errors; Core 50/50 passed; Planner 10/10 passed; focused ACL/recovery 3/3 passed; elevated Windows Security 28/28 passed; all reported skip 0.
Operational evidence: the updated Release Bundle completed MCP Task 9b4a59a9-1f9b-4362-85f4-7b858899b488. ACL apply/restore decreased from 3,583/3,583 events taking 72/57 seconds to 3/3 root_inheritance events completing within the same second. WFP residual filters were 0 and the Task journal was removed.
Release: refreshed the win-x64 self-contained folder bundle, clean ZIP, manifest, and checksum while preserving the local agent.yaml. Agent.Windows.dll SHA-256 is 24FDDD5727D5455170B6742B1D21E023A0BD70F92C225A2681BB5832C7AC0824; ZIP SHA-256 is EF7684C1A5A6155B774F4DF23CB08E9D1CF676351BF2B7D0F57915304495F456. No dependencies were added.
| -rw-r--r-- | doc/Design.md | 6 | ||||
| -rw-r--r-- | plans/ImplementationPlan.md | 3 | ||||
| -rw-r--r-- | src/Agent.Windows/Security/TaskAclManager.cs | 23 | ||||
| -rw-r--r-- | src/Agent.Windows/Tools/WindowsToolBackend.cs | 32 | ||||
| -rw-r--r-- | tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs | 6 | ||||
| -rw-r--r-- | tests/Agent.Windows.Tests/SecurityPolicyTests.cs | 147 |
6 files changed, 169 insertions, 48 deletions
diff --git a/doc/Design.md b/doc/Design.md index 693cdeb..a908441 100644 --- a/doc/Design.md +++ b/doc/Design.md @@ -477,8 +477,10 @@ network: - AppContainer外の通常user tokenに戻るbreakawayを許可しない。 ### 11.2 NTFS ACL - Policy Engineが許可pathから必要ACEを導出する。 -- 既存DACL全体を書き換えず、アプリケーションSID向けの必要ACEだけをtransactionalに追加する。 -- 元security descriptorまたは追加ACEの逆操作情報をRecovery Journalへ記録する。 +- 許可pathがdirectoryの場合、そのrootへ`ContainerInherit | ObjectInherit`付きのApplication SID ACEを1件だけtransactionalに追加し、既存およびTask中に作成されたdescendantへ継承させる。descendantごとのDACL列挙・snapshot・明示ACE追加は行わない。 +- 許可pathがfileの場合は、そのfileだけへ継承なしACEを追加する。異なる権限を持つnested rootは別scopeとして逆順cleanupする。 +- protected DACLを持つdescendantの継承保護は解除しない。そのpathはOS拒否のままとし、Sandbox権限を暗黙に拡張しない。 +- rootの元security descriptorをRecovery Journalへ1件記録する。 - Task終了で追加分を除去し、元状態との整合を検査する。 ### 11.3 Job Object - TaskごとにJob Objectを1つ作成する。 diff --git a/plans/ImplementationPlan.md b/plans/ImplementationPlan.md index 8127701..5af7606 100644 --- a/plans/ImplementationPlan.md +++ b/plans/ImplementationPlan.md @@ -364,7 +364,7 @@ LLM→Tool→Policyの境界が固定され、Policyを通らない副作用経è - FilesystemPolicy R/W/E/Delete - canonical path - reparse point防御 -- NTFS ACL +- NTFS ACL(承認directory rootへの継承ACE、root単位snapshot、protected descendantのfail-safe維持) - Process approval canonical path + SHA-256 - shell toggle - Job Object @@ -407,6 +407,7 @@ ST-01〜11、16全PASS。Security不具合を残したまま進行禁止。 - Recovery Journal - CLEAN / PREPARING / APPLIED / RUNNING / CLEANING / RECOVERY_REQUIRED - ACL適用前journal flush +- ACL journalは承認root単位とし、既存・新規descendantの継承とroot復元を検証 - Task Resource ownership - cleanup逆順処理 - 起動前Recovery diff --git a/src/Agent.Windows/Security/TaskAclManager.cs b/src/Agent.Windows/Security/TaskAclManager.cs index f01e859..2b091a6 100644 --- a/src/Agent.Windows/Security/TaskAclManager.cs +++ b/src/Agent.Windows/Security/TaskAclManager.cs @@ -26,7 +26,13 @@ public sealed class TaskAclManager(SecurityIdentifier appContainerSid, ISecurity security.AddAccessRule(rule); WriteSecurity(canonical, security); if (journalEntry is not null) journal!.MarkApplied(journalEntry.Value); - audit.Write(new(DateTimeOffset.UtcNow, taskId, "acl.apply", "applied", new Dictionary<string, string> { ["path"] = canonical, ["sid"] = appContainerSid.Value, ["rights"] = rights.ToString() })); + audit.Write(new(DateTimeOffset.UtcNow, taskId, "acl.apply", "applied", new Dictionary<string, string> + { + ["path"] = canonical, + ["sid"] = appContainerSid.Value, + ["rights"] = rights.ToString(), + ["scope"] = directory ? "root_inheritance" : "file" + })); return new(canonical, original, journalEntry); } @@ -39,21 +45,6 @@ public sealed class TaskAclManager(SecurityIdentifier appContainerSid, ISecurity audit.Write(new(DateTimeOffset.UtcNow, taskId, "acl.restore", "restored", new Dictionary<string, string> { ["path"] = snapshot.Path })); } - public void NormalizeNewEntry(Guid taskId, string path) - { - string canonical = CanonicalPath.Resolve(path); - FileSystemSecurity security = ReadSecurity(canonical); - security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); - foreach (FileSystemAccessRule rule in security.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier)) - .OfType<FileSystemAccessRule>() - .Where(rule => rule.IdentityReference == appContainerSid) - .ToArray()) - security.RemoveAccessRuleSpecific(rule); - security.SetAccessRuleProtection(isProtected: false, preserveInheritance: false); - WriteSecurity(canonical, security); - audit.Write(new(DateTimeOffset.UtcNow, taskId, "acl.normalize", "normalized", new Dictionary<string, string> { ["path"] = canonical })); - } - private static FileSystemSecurity ReadSecurity(string path) => Directory.Exists(path) ? FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(path), AccessControlSections.Access) : FileSystemAclExtensions.GetAccessControl(new FileInfo(path), AccessControlSections.Access); diff --git a/src/Agent.Windows/Tools/WindowsToolBackend.cs b/src/Agent.Windows/Tools/WindowsToolBackend.cs index a21544b..c0d1561 100644 --- a/src/Agent.Windows/Tools/WindowsToolBackend.cs +++ b/src/Agent.Windows/Tools/WindowsToolBackend.cs @@ -49,8 +49,10 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit // Do not rewrite a file ACL when execute is the only requested capability; CreateProcess remains // constrained by AppContainer access plus canonical path/SHA-256 approval. if (File.Exists(path) && capabilities == FileCapability.Execute) continue; - context.Roots.Add(path); - foreach (string target in ExistingAclTargets(path)) context.Acls.Add(acl.Grant(taskId, target, capabilities)); + // A single inheritable ACE on an approved directory covers existing and newly-created + // descendants. Snapshotting and rewriting every child ACL is both redundant and + // prohibitively expensive for runtime trees such as Python installations. + context.Acls.Add(acl.Grant(taskId, path, capabilities)); } IReadOnlyList<NetworkEndpoint> allowances = settings.NetworkMode == "allow" ? await NetworkAllowanceSnapshot.ResolveAsync(settings.NetworkAllowances, cancellationToken).ConfigureAwait(false) @@ -193,11 +195,6 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit List<Exception> failures = []; try { context.Network?.Dispose(); } catch (Exception ex) { failures.Add(ex); } foreach (AclSnapshot snapshot in context.Acls.AsEnumerable().Reverse()) try { context.Acl.Restore(taskId, snapshot); } catch (Exception ex) { failures.Add(ex); } - var existingSnapshots = context.Acls.Select(static snapshot => snapshot.Path).ToHashSet(StringComparer.OrdinalIgnoreCase); - foreach (string entry in context.Roots.SelectMany(ExistingAclTargets).Distinct(StringComparer.OrdinalIgnoreCase) - .Where(entry => !existingSnapshots.Contains(entry)) - .OrderBy(static entry => entry.Count(character => character == Path.DirectorySeparatorChar))) - try { context.Acl.NormalizeNewEntry(taskId, entry); } catch (Exception ex) { failures.Add(ex); } if (failures.Count == 0) context.Journal.CompleteAndDelete(); await Task.CompletedTask; if (failures.Count > 0) throw new AggregateException("Task resource cleanup failed.", failures); @@ -215,26 +212,6 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit var regex = new Regex(query, RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1)); return files.Where(path => IsTextMatch(path, regex.IsMatch)); } - private static IEnumerable<string> ExistingAclTargets(string path) - { - if (File.Exists(path)) - { - yield return path; - yield break; - } - if (!Directory.Exists(path)) yield break; - var options = new EnumerationOptions - { - RecurseSubdirectories = true, - IgnoreInaccessible = false, - ReturnSpecialDirectories = false, - AttributesToSkip = FileAttributes.ReparsePoint - }; - foreach (string entry in Directory.EnumerateFileSystemEntries(path, "*", options) - .OrderByDescending(static entry => entry.Count(character => character == Path.DirectorySeparatorChar))) - yield return entry; - yield return path; - } private static string ApplyPatch(string path, string patch) { string current = File.Exists(path) ? File.ReadAllText(path) : string.Empty; @@ -251,7 +228,6 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit public TaskResourceJournal Journal { get; } = journal; public TaskAclManager Acl { get; } = acl; public List<AclSnapshot> Acls { get; } = []; - public HashSet<string> Roots { get; } = new(StringComparer.OrdinalIgnoreCase); public TaskNetworkPolicy? Network { get; set; } } } diff --git a/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs b/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs index 7dd3629..dd170ab 100644 --- a/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs +++ b/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs @@ -26,6 +26,12 @@ internal static class CrashScenarioBootstrap var acl = new TaskAclManager(AppContainerProfile.EnsureFixed().Sid, new MemorySecurityAuditSink(), journal); _ = acl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write); break; + case "acl-inheritance": + var inheritedAcl = new TaskAclManager( + new System.Security.Principal.SecurityIdentifier("S-1-15-2-330000001-330000002-330000003-330000004-330000005-330000006-330000007"), + new MemorySecurityAuditSink(), journal); + _ = inheritedAcl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write); + break; case "acl-before": byte[] descriptor = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(Path.Combine(root, "resource")), AccessControlSections.Access) .GetSecurityDescriptorBinaryForm(); diff --git a/tests/Agent.Windows.Tests/SecurityPolicyTests.cs b/tests/Agent.Windows.Tests/SecurityPolicyTests.cs index d54ab13..8d920b2 100644 --- a/tests/Agent.Windows.Tests/SecurityPolicyTests.cs +++ b/tests/Agent.Windows.Tests/SecurityPolicyTests.cs @@ -192,6 +192,75 @@ public sealed class SecurityPolicyTests } [Fact] + public void Root_acl_inheritance_covers_existing_and_new_descendants_and_restores_exactly() + { + using TestDirectory directory = new(); + using TestDirectory state = new(); + string existingDirectory = Path.Combine(directory.Path, "existing"); + string existingFile = Path.Combine(existingDirectory, "before.txt"); + Directory.CreateDirectory(existingDirectory); + File.WriteAllText(existingFile, "before"); + byte[] rootBefore = AccessDescriptor(directory.Path); + byte[] existingBefore = AccessDescriptor(existingFile); + var sid = new System.Security.Principal.SecurityIdentifier( + "S-1-15-2-310000001-310000002-310000003-310000004-310000005-310000006-310000007"); + var audit = new MemorySecurityAuditSink(); + Guid taskId = Guid.NewGuid(); + TaskResourceJournal journal = TaskResourceJournal.Create(Path.Combine(state.Path, "journals"), taskId); + TaskAclManager manager = new(sid, audit, journal); + + AclSnapshot snapshot = manager.Grant(taskId, directory.Path, FileCapability.Read | FileCapability.Write); + JournalEntry aclEntry = Assert.Single(journal.Entries); + Assert.Equal("acl", aclEntry.Kind); + Assert.Equal(directory.Path, aclEntry.Target, ignoreCase: true); + Assert.Contains(AccessRules(directory.Path, sid), static rule => !rule.IsInherited); + Assert.Contains(AccessRules(existingFile, sid), static rule => rule.IsInherited); + + string createdDirectory = Path.Combine(directory.Path, "created"); + string createdFile = Path.Combine(createdDirectory, "during.txt"); + Directory.CreateDirectory(createdDirectory); + File.WriteAllText(createdFile, "during"); + Assert.Contains(AccessRules(createdFile, sid), static rule => rule.IsInherited); + + manager.Restore(taskId, snapshot); + journal.CompleteAndDelete(); + + Assert.Equal(Convert.ToHexString(rootBefore), Convert.ToHexString(AccessDescriptor(directory.Path))); + Assert.Equal(Convert.ToHexString(existingBefore), Convert.ToHexString(AccessDescriptor(existingFile))); + Assert.Empty(AccessRules(existingFile, sid)); + Assert.Empty(AccessRules(createdFile, sid)); + SecurityAuditEvent applied = Assert.Single(audit.Events, item => item.Event == "acl.apply"); + Assert.Equal("root_inheritance", applied.Data["scope"]); + Assert.Single(audit.Events, item => item.Event == "acl.restore"); + Assert.Empty(Directory.EnumerateFiles(Path.Combine(state.Path, "journals"))); + } + + [Fact] + public void Root_acl_inheritance_does_not_override_a_protected_descendant() + { + using TestDirectory directory = new(); + string protectedDirectory = Path.Combine(directory.Path, "protected"); + Directory.CreateDirectory(protectedDirectory); + DirectorySecurity protectedAcl = FileSystemAclExtensions.GetAccessControl( + new DirectoryInfo(protectedDirectory), AccessControlSections.Access); + protectedAcl.SetAccessRuleProtection(isProtected: true, preserveInheritance: true); + FileSystemAclExtensions.SetAccessControl(new DirectoryInfo(protectedDirectory), protectedAcl); + var sid = new System.Security.Principal.SecurityIdentifier( + "S-1-15-2-320000001-320000002-320000003-320000004-320000005-320000006-320000007"); + TaskAclManager manager = new(sid, new MemorySecurityAuditSink()); + + AclSnapshot snapshot = manager.Grant(Guid.NewGuid(), directory.Path, FileCapability.Read); + try + { + Assert.Empty(AccessRules(protectedDirectory, sid)); + } + finally + { + manager.Restore(Guid.NewGuid(), snapshot); + } + } + + [Fact] public void Brokered_process_runs_in_fixed_app_container_with_task_acl() { AppContainerProfile profile = AppContainerProfile.EnsureFixed(); @@ -273,14 +342,18 @@ public sealed class SecurityPolicyTests string journals = Path.Combine(directory.Path, "journals"); string resource = Path.Combine(directory.Path, "resource"); Directory.CreateDirectory(resource); + string child = Path.Combine(resource, "existing.txt"); + File.WriteAllText(child, "existing"); byte[] aclBefore = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(resource), AccessControlSections.Access) .GetSecurityDescriptorBinaryForm(); + byte[] childAclBefore = AccessDescriptor(child); var audit = new MemorySecurityAuditSink(); Guid taskId = Guid.NewGuid(); TaskResourceJournal journal = TaskResourceJournal.Create(journals, taskId); var acl = new TaskAclManager(profile.Sid, audit, journal); _ = acl.Grant(taskId, resource, FileCapability.Read | FileCapability.Write); + Assert.Contains(AccessRules(child, profile.Sid), static rule => rule.IsInherited); Assert.False(LoopbackExemption.Contains(profile.Sid)); Guid loopbackEntry = journal.Prepare("network.loopback_exemption", profile.Sid.Value, @@ -307,6 +380,8 @@ public sealed class SecurityPolicyTests byte[] aclAfter = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(resource), AccessControlSections.Access) .GetSecurityDescriptorBinaryForm(); Assert.Equal(Convert.ToHexString(aclBefore), Convert.ToHexString(aclAfter)); + Assert.Equal(Convert.ToHexString(childAclBefore), Convert.ToHexString(AccessDescriptor(child))); + Assert.Empty(AccessRules(child, profile.Sid)); Assert.Empty(Directory.EnumerateFiles(journals)); } @@ -324,9 +399,12 @@ public sealed class SecurityPolicyTests string resourceDirectory = Path.Combine(directory.Path, "resource"); string resourceFile = Path.Combine(directory.Path, "resource.txt"); Directory.CreateDirectory(resourceDirectory); + string childFile = Path.Combine(resourceDirectory, "child.txt"); + File.WriteAllText(childFile, "child-before-crash"); File.WriteAllText(resourceFile, "before-crash"); byte[] aclBefore = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(resourceDirectory), AccessControlSections.Access) .GetSecurityDescriptorBinaryForm(); + byte[] childAclBefore = AccessDescriptor(childFile); Guid taskId = Guid.NewGuid(); using var child = new Process { @@ -358,6 +436,8 @@ public sealed class SecurityPolicyTests byte[] aclAfter = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(resourceDirectory), AccessControlSections.Access) .GetSecurityDescriptorBinaryForm(); Assert.Equal(Convert.ToHexString(aclBefore), Convert.ToHexString(aclAfter)); + Assert.Equal(Convert.ToHexString(childAclBefore), Convert.ToHexString(AccessDescriptor(childFile))); + Assert.Empty(AccessRules(childFile, AppContainerProfile.EnsureFixed().Sid)); Assert.False(LoopbackExemption.Contains(AppContainerProfile.EnsureFixed().Sid)); string backups = Path.Combine(directory.Path, "backups"); Assert.True(!Directory.Exists(backups) || !Directory.EnumerateFileSystemEntries(backups).Any()); @@ -371,6 +451,51 @@ public sealed class SecurityPolicyTests } } + [Fact] + public void Independent_process_crash_recovers_root_inheritance_from_existing_child() + { + using TestDirectory directory = new(); + string resource = Path.Combine(directory.Path, "resource"); + string childFile = Path.Combine(resource, "child.txt"); + Directory.CreateDirectory(resource); + File.WriteAllText(childFile, "before-crash"); + byte[] rootBefore = AccessDescriptor(resource); + byte[] childBefore = AccessDescriptor(childFile); + var sid = new System.Security.Principal.SecurityIdentifier( + "S-1-15-2-330000001-330000002-330000003-330000004-330000005-330000006-330000007"); + Guid taskId = Guid.NewGuid(); + using var child = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = Environment.ProcessPath!, + UseShellExecute = false, + CreateNoWindow = true, + Environment = + { + ["AGENT_MCP_CRASH_SCENARIO"] = "acl-inheritance", + ["AGENT_MCP_CRASH_ROOT"] = directory.Path, + ["AGENT_MCP_CRASH_TASK"] = taskId.ToString("D") + } + } + }; + + Assert.True(child.Start()); + Assert.True(child.WaitForExit(30_000)); + Assert.NotEqual(0, child.ExitCode); + Assert.False(File.Exists(Path.Combine(directory.Path, "crash-error.txt")), + File.Exists(Path.Combine(directory.Path, "crash-error.txt")) ? File.ReadAllText(Path.Combine(directory.Path, "crash-error.txt")) : string.Empty); + Assert.Contains(AccessRules(childFile, sid), static rule => rule.IsInherited); + + RecoveryResult recovered = new RecoveryManager(Path.Combine(directory.Path, "journals"), new MemorySecurityAuditSink()).RecoverAll(); + + Assert.True(recovered.IsClean, string.Join(Environment.NewLine, recovered.Errors)); + Assert.Equal(Convert.ToHexString(rootBefore), Convert.ToHexString(AccessDescriptor(resource))); + Assert.Equal(Convert.ToHexString(childBefore), Convert.ToHexString(AccessDescriptor(childFile))); + Assert.Empty(AccessRules(childFile, sid)); + Assert.Empty(Directory.EnumerateFiles(Path.Combine(directory.Path, "journals"))); + } + private static bool ProcessExists(int pid) { try { using Process process = Process.GetProcessById(pid); return !process.HasExited; } @@ -440,7 +565,8 @@ public sealed class SecurityPolicyTests string backups = Path.Combine(directory.Path, "backups"); Assert.True(!Directory.Exists(backups) || !Directory.EnumerateFileSystemEntries(backups).Any()); Assert.Contains(audit.Events, item => item.Event == "wfp.remove" && item.Data["residual_filters"] == "0"); - Assert.Contains(audit.Events, item => item.Event == "acl.restore"); + Assert.Single(audit.Events, item => item.Event == "acl.apply"); + Assert.Single(audit.Events, item => item.Event == "acl.restore"); } [Fact(Timeout = 60_000)] @@ -514,6 +640,25 @@ public sealed class SecurityPolicyTests private static RestrictedProcessResult Curl(RestrictedProcessLauncher launcher, Guid taskId, string curl, string url, string cwd) => launcher.Run(taskId, curl, ["--noproxy", "*", "--silent", "--show-error", "--max-time", "2", url], cwd, TimeSpan.FromSeconds(10)); + private static byte[] AccessDescriptor(string path) + { + FileSystemSecurity security = Directory.Exists(path) + ? FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(path), AccessControlSections.Access) + : FileSystemAclExtensions.GetAccessControl(new FileInfo(path), AccessControlSections.Access); + return security.GetSecurityDescriptorBinaryForm(); + } + + private static FileSystemAccessRule[] AccessRules(string path, System.Security.Principal.SecurityIdentifier sid) + { + FileSystemSecurity security = Directory.Exists(path) + ? FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(path), AccessControlSections.Access) + : FileSystemAclExtensions.GetAccessControl(new FileInfo(path), AccessControlSections.Access); + return security.GetAccessRules(includeExplicit: true, includeInherited: true, typeof(System.Security.Principal.SecurityIdentifier)) + .OfType<FileSystemAccessRule>() + .Where(rule => rule.IdentityReference == sid && rule.AccessControlType == AccessControlType.Allow) + .ToArray(); + } + private sealed class TestDirectory : IDisposable { public TestDirectory() |
