我有一个应用程序在多个服务器上运行,应用了一些ACL。
问题是当多个服务器应用于相同的文件夹结构(即三个级别)时,通常只有第一级和第三级应用了ACL,但是没有例外。
我已经使用并行任务创建了一个测试(以模拟不同的服务器):
[TestMethod]
public void ApplyACL()
{
var baseDir = Path.Combine(Path.GetTempPath(), "ACL-PROBLEM");
if (Directory.Exists(baseDir))
{
Directory.Delete(baseDir, true);
}
var paths = new[]
{
Path.Combine(baseDir, "LEVEL-1"),
Path.Combine(baseDir, "LEVEL-1", "LEVEL-2"),
Path.Combine(baseDir, "LEVEL-1", "LEVEL-2", "LEVEL-3")
};
//create folders and files, so the ACL takes some time to apply
foreach (var dir in paths)
{
Directory.CreateDirectory(dir);
for (int i = 0; i < 1000; i++)
{
var id = string.Format("{0:000}", i);
File.WriteAllText(Path.Combine(dir, id + ".txt"), id);
}
}
var sids = new[]
{
"S-1-5-21-448539723-725345543-1417001333-1111111",
"S-1-5-21-448539723-725345543-1417001333-2222222",
"S-1-5-21-448539723-725345543-1417001333-3333333"
};
var taskList = new List<Task>();
for (int i = 0; i < paths.Length; i++)
{
taskList.Add(CreateTask(i + 1, paths[i], sids[i]));
}
Parallel.ForEach(taskList, t => t.Start());
Task.WaitAll(taskList.ToArray());
var output = new StringBuilder();
var failed = false;
for (int i = 0; i < paths.Length; i++)
{
var ok = Directory.GetAccessControl(paths[i])
.GetAccessRules(true, false, typeof(SecurityIdentifier))
.OfType<FileSystemAccessRule>()
.Any(f => f.IdentityReference.Value == sids[i]);
if (!ok)
{
failed = true;
}
output.AppendLine(paths[i].Remove(0, baseDir.Length + 1) + " --> " + (ok ? "OK" : "ERROR"));
}
Debug.WriteLine(output);
if (failed)
{
Assert.Fail();
}
}
private static Task CreateTask(int i, string path, string sid)
{
return new Task(() =>
{
var start = DateTime.Now;
Debug.WriteLine("Task {0} start: {1:HH:mm:ss.fffffff}", i, start);
var fileSystemAccessRule = new FileSystemAccessRule(new SecurityIdentifier(sid),
FileSystemRights.Modify | FileSystemRights.Synchronize,
InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
PropagationFlags.None,
AccessControlType.Allow);
var directorySecurity = Directory.GetAccessControl(path);
directorySecurity.ResetAccessRule(fileSystemAccessRule);
Directory.SetAccessControl(path, directorySecurity);
Debug.WriteLine("Task {0} finish: {1:HH:mm:ss.fffffff} ({2} ms)", i, DateTime.Now, (DateTime.Now - start).TotalMilliseconds);
});
}
我遇到了同样的问题:通常(但并非总是)只有第一级和第三级已经应用了ACL。
为什么会这样,我该如何解决这个问题?
答案 0 :(得分:2)
这是一个有趣的谜题。
我已经启动了测试,问题几乎每次都会重现。并且ACL通常也不适用于LEVEL-3。
但是,如果任务不并行运行,则问题不会重现。 此外,如果目录不包含这1000个文件,则问题的重现次数会少得多。
此类行为与经典race condition非常相似。
我没有找到关于此主题的任何明确信息,但似乎在重叠目录树上应用ACL不是线程安全的操作。
为了确认这一点,我们需要分析SetAccessControl()
(或者更确切地说是基础Windows API调用)的实现。但是,让我们试着想象它可能是什么。
SetAccessControl()
记录调用DirectorySecurity
。SetAccessControl()
也是如此。最后它将覆盖在步骤4中创建的记录。当然,描述的流程只是一个假设。我们需要NTFS或Windows内部专家来确认这一点。
但观察到的行为几乎肯定表明竞争状况。只是避免在重叠的目录树上并行应用ACL并且睡得好。
答案 1 :(得分:2)
Directory.SetAccessControl
在内部调用Win32 API函数SetSecurityInfo
:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa379588.aspx
上述文件的重要部分:
如果要设置自主访问控制列表(DACL)或对象的系统访问控制列表(SACL)中的任何元素,系统会自动将任何可继承的访问控制条目(ACE)传播到现有子对象,根据ACE继承规则。
子对象的枚举(CodeFuller已经描述过)在低级函数SetSecurityInfo
中完成。更详细地说,这个函数调用系统DLL NTMARTA.DLL,它执行所有脏工作。
其背景是继承,这是一种“伪继承”,出于性能原因。每个对象不仅包含“自己的”ACE,还包含继承的ACE(在资源管理器中显示为灰色的ACE)。所有这些继承都在ACL设置期间完成,而不是在运行时ACL解析/检查期间完成。
Microsoft以前的这个决定也是以下问题的触发因素(Windows管理员应该知道这一点):
如果将目录树移动到设置了不同ACL的文件系统中的另一个位置,则移动的try的对象的ACL将不会更改。
所以说,继承的权限是错误的,它们不再与父级的ACL匹配。
此继承不是由InheritanceFlags
定义的,而是由。{1}}定义的
SetAccessRuleProtection
。
添加CodeFuller的答案:
&gt;&gt;枚举完成后,将内部目录安全记录分配给目录。
这个枚举不仅仅是对子对象的纯读取,每个子对象的ACL都是 SET 。
因此,问题是Windows ACL处理的内部工作所固有的:
SetSecurityInfo
检查父目录中是否应继承所有ACE,然后进行递归并将这些可继承的ACE应用于所有子对象。
我知道这是因为我编写了一个工具来设置完整文件系统的ACL(包含数百万个文件),这些工具使用我们称之为“托管文件夹”的文件。我们可以使用具有自动计算列表权限的非常复杂的ALC。
为了设置文件和文件夹的ACL我使用SetKernelObjectSecurity
。此API通常不应用于文件系统,因为它不处理该继承的东西。所以你必须自己做。但是,如果你知道你做了什么并且你正确地做了,那么它是在每种情况下在文件树上设置ACL的唯一可靠方法。
实际上,可能存在SetSecurityInfo
无法正确设置这些对象的情况(子对象中的ACL /条目损坏/无效)。
现在来自Anderson Pimentel的代码:
从上面可以清楚地看出,并行设置只有在继承被阻止时才能起作用
在每个目录级别。
但是,它只能调用
dirSecurity.SetAccessRuleProtection(true, true);
在任务中,因为这个电话可能会迟到。
如果在启动任务之前调用上述语句,则代码可以正常工作。
坏消息是,使用C#完成此调用也会进行完整的递归。
除了使用PInvoke直接调用低级安全功能外,C#似乎没有真正引人注目的解决方案。
但这是另一个故事。
以及不同服务器设置ACL的初始问题:
如果我们知道背后的意图以及您希望得到的ALC是什么,我们也许可以找到一种方法。
让我知道。
答案 2 :(得分:1)
介绍一个锁。您有共享文件系统,因此在进程更改文件夹时使用.NET锁定:
using (new FileStream(lockFile, FileMode.Open, FileAccess.Read, FileShare.None))
{
// file locked
}
在你的代码中添加初始化:
var lockFile = Path.Combine(baseDir, ".lock"); // just create a file
File.WriteAllText(lockFile, "lock file");
并将众所周知的锁定文件传递给您的任务。 然后等待文件在每个进程中解锁:
private static Task CreateTask(int i, string path, string sid, string lockFile)
{
return new Task(() =>
{
var start = DateTime.Now;
Debug.WriteLine("Task {0} start: {1:HH:mm:ss.fffffff}", i, start);
Task.WaitAll(WaitForFileToUnlock(lockFile, () =>
{
var fileSystemAccessRule = new FileSystemAccessRule(new SecurityIdentifier(sid),
FileSystemRights.Modify | FileSystemRights.Synchronize,
InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
PropagationFlags.None,
AccessControlType.Allow);
var directorySecurity = Directory.GetAccessControl(path);
directorySecurity.ResetAccessRule(fileSystemAccessRule);
Directory.SetAccessControl(path, directorySecurity);
}));
Debug.WriteLine("Task {0} finish: {1:HH:mm:ss.fffffff} ({2} ms)", i, DateTime.Now, (DateTime.Now - start).TotalMilliseconds);
});
}
private static async Task WaitForFileToUnlock(string lockFile, Action runWhenUnlocked)
{
while (true)
{
try
{
using (new FileStream(lockFile, FileMode.Open, FileAccess.Read, FileShare.None))
{
runWhenUnlocked();
}
return;
}
catch (IOException exception)
{
await Task.Delay(100);
}
}
}
通过这些更改,单元测试通过。
您可以在各个级别上添加更多锁,以使流程最有效 - 类似于层次结构锁定逻辑。