静默应用ACL失败(有时)

时间:2017-11-21 12:38:30

标签: c# file-permissions acl distributed-computing

我有一个应用程序在多个服务器上运行,应用了一些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。

为什么会这样,我该如何解决这个问题?

3 个答案:

答案 0 :(得分:2)

这是一个有趣的谜题。

我已经启动了测试,问题几乎每次都会重现。并且ACL通常也不适用于LEVEL-3。

但是,如果任务不并行运行,则问题不会重现。 此外,如果目录不包含这1000个文件,则问题的重现次数会少得多。

此类行为与经典race condition非常相似。

我没有找到关于此主题的任何明确信息,但似乎在重叠目录树上应用ACL不是线程安全的操作。

为了确认这一点,我们需要分析SetAccessControl()(或者更确切地说是基础Windows API调用)的实现。但是,让我们试着想象它可能是什么。

    为给定目录和SetAccessControl()记录调用
  1. DirectorySecurity
  2. 它创建一些内部结构(文件系统对象)并用提供的数据填充它。
  3. 然后它开始枚举子对象(目录和文件)。这种枚举部分由任务执行时间确认。 task3约为500毫秒,task2为1000毫秒,task1为1500毫秒。
  4. 枚举完成后,将内部目录安全记录分配给目录。
  5. 但同时,对父目录调用的SetAccessControl()也是如此。最后它将覆盖在步骤4中创建的记录。
  6. 当然,描述的流程只是一个假设。我们需要NTFS或Windows内部专家来确认这一点。

    但观察到的行为几乎肯定表明竞争状况。只是避免在重叠的目录树上并行应用ACL并且睡得好。

答案 1 :(得分:2)

Directory.SetAccessControl在内部调用Win32 API函数SetSecurityInfohttps://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);
        }
    }
}

通过这些更改,单元测试通过。

您可以在各个级别上添加更多锁,以使流程最有效 - 类似于层次结构锁定逻辑。