在.NET中检查目录和文件的写入权限

90

在我的.NET 2.0应用程序中,我需要检查是否有足够的权限来创建和写入文件到一个目录。为此,我有以下函数试图创建一个文件并写入一个字节,然后删除它自己以测试权限是否存在。

我认为最好的方法是实际尝试并捕获任何出现的异常来进行检查。但是,我对于使用一般的Exception catch并不是特别满意,那么有没有更好或更常见的做法呢?

private const string TEMP_FILE = "\\tempFile.tmp";

/// <summary>
/// Checks the ability to create and write to a file in the supplied directory.
/// </summary>
/// <param name="directory">String representing the directory path to check.</param>
/// <returns>True if successful; otherwise false.</returns>
private static bool CheckDirectoryAccess(string directory)
{
    bool success = false;
    string fullPath = directory + TEMP_FILE;

    if (Directory.Exists(directory))
    {
        try
        {
            using (FileStream fs = new FileStream(fullPath, FileMode.CreateNew, 
                                                            FileAccess.Write))
            {
                fs.WriteByte(0xff);
            }

            if (File.Exists(fullPath))
            {
                File.Delete(fullPath);
                success = true;
            }
        }
        catch (Exception)
        {
            success = false;
        }
    }

谢谢你提供的代码,不过有一点需要注意,如果用户能够写入但无法删除文件,调用者可能会得出错误的结论认为缺少写入权限。我建议使用FileMode.Create来替代删除文件的操作。显然,你现在不需要这段代码了,但我写下这些话是为了未来的读者受益。 - n00b
3
string fullPath = directory + TEMP_FILE; Please use Path.Combine method instead of concatenating strings to get fullPath. Path.Combine(directory, TEMP_FILE) - nomail
如果有人打卡然后第二天再打卡怎么办?如果他们打卡然后两天后再打卡怎么办?我相信人们不应该这样做,但是这种行为应该被定义。 - Scott Hannen
9个回答

52

Directory.GetAccessControl(path)可以满足你的需求。

public static bool HasWritePermissionOnDir(string path)
{
    var writeAllow = false;
    var writeDeny = false;
    var accessControlList = Directory.GetAccessControl(path);
    if (accessControlList == null)
        return false;
    var accessRules = accessControlList.GetAccessRules(true, true, 
                                typeof(System.Security.Principal.SecurityIdentifier));
    if (accessRules ==null)
        return false;

    foreach (FileSystemAccessRule rule in accessRules)
    {
        if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) 
            continue;

        if (rule.AccessControlType == AccessControlType.Allow)
            writeAllow = true;
        else if (rule.AccessControlType == AccessControlType.Deny)
            writeDeny = true;
    }

    return writeAllow && !writeDeny;
}

(FileSystemRights.Write & rights) == FileSystemRights.Write 是在使用一种叫做“Flags”的东西,如果你不知道它是什么,你应该去了解一下 :)


9
如果您无法获取目录上的ACL,那当然会抛出异常。 - blowdart
5
它检查什么?那个目录有写权限,但是针对哪个用户? :) - Ivan G.
3
如果您只想查看当前用户是否具有写入权限,那么它是有效的。 - Donny V.
7
尝试在Windows 7上使用非管理员应用程序在系统磁盘上运行此程序,它将返回true,但当您尝试写入C:\时,您将收到一个异常,指出您没有访问权限! - Peter
1
这在 .Net Core 3.1 中已经不再适用。GetAccessControl(path) 不再是 Directory 类的成员。 它也不是 DirectoryInfo 类的成员。我已经苦苦思索了几个小时,试图找到一种检查是否可以搜索目录的方法。太好了,微软!Directory.Exists 现在始终返回 true,无论有没有权限。 - Richard Robertson
显示剩余8条评论

36

拒绝优先于允许。局部规则优先于继承规则。我看过很多解决方案(包括一些在这里显示的答案),但没有一个考虑到规则是否被继承。因此,我建议采用以下方法来考虑规则继承(整洁地封装到一个类中):

public class CurrentUserSecurity
{
    WindowsIdentity _currentUser;
    WindowsPrincipal _currentPrincipal;

    public CurrentUserSecurity()
    {
        _currentUser = WindowsIdentity.GetCurrent();
        _currentPrincipal = new WindowsPrincipal(_currentUser);
    }

    public bool HasAccess(DirectoryInfo directory, FileSystemRights right)
    {
        // Get the collection of authorization rules that apply to the directory.
        AuthorizationRuleCollection acl = directory.GetAccessControl()
            .GetAccessRules(true, true, typeof(SecurityIdentifier));
        return HasFileOrDirectoryAccess(right, acl);
    }

    public bool HasAccess(FileInfo file, FileSystemRights right)
    {
        // Get the collection of authorization rules that apply to the file.
        AuthorizationRuleCollection acl = file.GetAccessControl()
            .GetAccessRules(true, true, typeof(SecurityIdentifier));
        return HasFileOrDirectoryAccess(right, acl);
    }

    private bool HasFileOrDirectoryAccess(FileSystemRights right,
                                          AuthorizationRuleCollection acl)
    {
        bool allow = false;
        bool inheritedAllow = false;
        bool inheritedDeny = false;

        for (int i = 0; i < acl.Count; i++) {
            var currentRule = (FileSystemAccessRule)acl[i];
            // If the current rule applies to the current user.
            if (_currentUser.User.Equals(currentRule.IdentityReference) ||
                _currentPrincipal.IsInRole(
                                (SecurityIdentifier)currentRule.IdentityReference)) {

                if (currentRule.AccessControlType.Equals(AccessControlType.Deny)) {
                    if ((currentRule.FileSystemRights & right) == right) {
                        if (currentRule.IsInherited) {
                            inheritedDeny = true;
                        } else { // Non inherited "deny" takes overall precedence.
                            return false;
                        }
                    }
                } else if (currentRule.AccessControlType
                                                  .Equals(AccessControlType.Allow)) {
                    if ((currentRule.FileSystemRights & right) == right) {
                        if (currentRule.IsInherited) {
                            inheritedAllow = true;
                        } else {
                            allow = true;
                        }
                    }
                }
            }
        }

        if (allow) { // Non inherited "allow" takes precedence over inherited rules.
            return true;
        }
        return inheritedAllow && !inheritedDeny;
    }
}

然而,我的经验是,在远程计算机上这并不总是奏效,因为您可能没有查询文件访问权限的权利。在这种情况下的解决方案是尝试创建临时文件,以便在使用“真实”文件之前了解访问权限;甚至只需尝试即可。


2
我认为这个答案是实现它的最佳方式,其他答案也使用相同的方法来获得结果,但由于只有这个答案计算了继承规则和本地规则,所以我猜这是最准确的。谢谢&祝贺。 - Tolga Evcimen
旧的线程。但是我该如何实际获取参数 FileSystemRights right 并传递到方法 HasAccess 中呢? - Breakwin
FileSystemRights 是一个枚举类型,包含诸如 ReadDataWriteData 等常量。您需要传入您感兴趣的枚举常量。它们是标志值,所以您可以像这样组合它们:FileSystemRights.ReadData | FileSystemRights.ListDirectory - Olivier Jacot-Descombes

28
RichardJason的回答有一定的参考价值。然而,你应该做的是计算运行代码的用户身份的有效权限。以上示例没有正确考虑群组成员身份等因素。
我相信Keith Brown在他的wiki版本(此时离线)的《.NET开发者Windows安全指南》中提供了相关代码。这也在他的《编程Windows安全》一书中详细讨论过。
计算有效权限并不是轻而易举的事情,你的代码尝试创建文件并捕获安全异常是最简单的方法。

3
这也是唯一可靠的方法,否则在检查和实际尝试保存之间,有人可能会更改权限(虽然不太可能)。 - Chris Chilvers
1
谢谢您。所以我应该对我的代码进行的唯一更改就是捕获安全异常而不是一般的“异常”吗? - Andy
@Andy - 是的,除非您想编写计算有效权限的代码,否则这是最简易的路径。 - Kev
3
为什么所有的东西都要这么复杂! - Vidar
事情只有在你理解它们之前才显得复杂。这并不难。只需使用Directory.GetAccessControl获取访问控制列表,然后获取访问规则,并查找所感兴趣的规则(如读、写等)上的允许或拒绝权限。如果您找不到规则,则没有访问权限。如果您至少找到一个允许,则具有访问权限,除非您找到单个拒绝。这是非常基本的安全原则。请参见Richard的另一个答案:https://dev59.com/rnM_5IYBdhLWcg3wq1CF#1281638 - Triynko
3
@Triynko - 我建议你阅读我引用的文章:https://groups.google.com/group/microsoft.public.dotnet.languages.csharp/msg/208347aa99a2de3b?hl=en - 计算“有效”权限并不像听起来那么简单。欢迎你提供一个答案来证明我错了。 - Kev

19

在这个问题中,Kev给出的被认可的答案并没有实际提供任何代码,只是指向了我无法访问的其他资源。所以这里是我最好的努力提供的函数。它实际上检查正在查看的权限是否为“写入”权限,并且当前用户属于适当的组。

它可能在网络路径或其他方面不完整,但对于我的目的来说已经足够,在“程序文件”下检查本地配置文件是否具有可写性:

using System.Security.Principal;
using System.Security.AccessControl;

private static bool HasWritePermission(string FilePath)
{
    try
    {
        FileSystemSecurity security;
        if (File.Exists(FilePath))
        {
            security = File.GetAccessControl(FilePath);
        }
        else
        {
            security = Directory.GetAccessControl(Path.GetDirectoryName(FilePath));
        }
        var rules = security.GetAccessRules(true, true, typeof(NTAccount));

        var currentuser = new WindowsPrincipal(WindowsIdentity.GetCurrent());
        bool result = false;
        foreach (FileSystemAccessRule rule in rules)
        {
            if (0 == (rule.FileSystemRights &
                (FileSystemRights.WriteData | FileSystemRights.Write)))
            {
                continue;
            }

            if (rule.IdentityReference.Value.StartsWith("S-1-"))
            {
                var sid = new SecurityIdentifier(rule.IdentityReference.Value);
                if (!currentuser.IsInRole(sid))
                {
                    continue;
                }
            }
            else
            {
                if (!currentuser.IsInRole(rule.IdentityReference.Value))
                {
                    continue;
                }
            }

            if (rule.AccessControlType == AccessControlType.Deny)
                return false;
            if (rule.AccessControlType == AccessControlType.Allow)
                result = true;
        }
        return result;
    }
    catch
    {
        return false;
    }
}

这个只适用于我情况下手动添加的账户名,而不适用于群组。 - Random
这与“(S-1-5-21-397955417-626881126-188441444-512)”类型格式有关吗?将字符串转换为SecurityIdentifier是否解决了您的问题?从您的评论中并不清楚它现在是否正常工作。 - Bryce Wagner
当您将“rule.IdentityReference.Value”作为currentuser.IsInRole()的参数时,您使用了IsInRole(string)方法,该方法尝试通过常规的“domain\user”值进行匹配。因此,您正在推送SID字符串而不是用户名字符串。但是,如果您在其前面使用我的代码行,您将获得SecurityIdentifier对象,该对象与给定SID的用户匹配。那个“string”参数重载对于开发人员来说是一个小陷阱,再次接受帐户或组名以人类可读格式而不是SID字符串表示。 - Random
问题在于 "new SecurityIdentifier(SDDLFormat)" 无法处理普通组名称(会抛出参数异常)。因此我加了一个检查,判断是否为 SDDL 格式。 - Bryce Wagner
@Bryce Wagner 应该删除其中一个“IF”语句,因为变量“sid”不在作用域内。如果(!currentuser.IsInRole(sid)) { 继续; } - Dmitry Dzygin
2
这个解决方案对我有用,但是在网络文件夹方面存在一个问题。该文件夹具有允许写入“BUILTIN\Administrators”的访问规则。由于我是本地站点的管理员,代码片段错误地返回了“true”。 - Ilia Barahovsky

7

在我看来,您需要像平常一样使用这些目录,但是不要在使用之前检查权限,而是提供正确的处理UnauthorizedAccessException的方式,并根据情况做出反应。这种方法更加简单且错误概率更小。


2
你可能想说的是“这种方法更容易且错误率更低。” - cjbarth

4

尝试使用我刚刚编写的C#片段:

using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string directory = @"C:\downloads";

            DirectoryInfo di = new DirectoryInfo(directory);

            DirectorySecurity ds = di.GetAccessControl();

            foreach (AccessRule rule in ds.GetAccessRules(true, true, typeof(NTAccount)))
            {
                Console.WriteLine("Identity = {0}; Access = {1}", 
                              rule.IdentityReference.Value, rule.AccessControlType);
            }
        }
    }
}

这里还有一个参考资料,可以帮助您了解如何在尝试写入目录之前检查权限。我的代码可能会给您提供一些想法。


typeof 返回一个对象的类型,这里是 NTAccount。https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/typeof 当调用 GetAccessRules() 时需要传入账户的类型。https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.directoryobjectsecurity.getaccessrules%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 - Jason Evans
为什么要使用 NTAccount?是否总是要使用 NTAccount - Kiquenet
在这种情况下,是的。NTAccount代表Windows PC上的用户帐户,这就是为什么我们需要它在上面的代码中。 - Jason Evans

1
根据此链接: http://www.authorcode.com/how-to-check-file-permission-to-write-in-c/ 使用现有的SecurityManager类更容易。
string FileLocation = @"C:\test.txt";
FileIOPermission writePermission = new FileIOPermission(FileIOPermissionAccess.Write, FileLocation);
if (SecurityManager.IsGranted(writePermission))
{
  // you have permission
}
else
{
 // permission is required!
}

但似乎它已经过时了,建议使用PermissionSet代替。

[Obsolete("IsGranted is obsolete and will be removed in a future release of the .NET Framework.  Please use the PermissionSet property of either AppDomain or Assembly instead.")]

1

由于当前版本的.Net Core/Standard中似乎缺少静态方法“GetAccessControl”,我不得不修改了@Bryce Wagner的答案(我采用了更现代的语法):

public static class PermissionHelper
{
  public static bool? CurrentUserHasWritePermission(string filePath)

     => new WindowsPrincipal(WindowsIdentity.GetCurrent())
        .SelectWritePermissions(filePath)
        .FirstOrDefault();


  private static IEnumerable<bool?> SelectWritePermissions(this WindowsPrincipal user, string filePath)
     => from rule in filePath
                    .GetFileSystemSecurity()
                    .GetAccessRules(true, true, typeof(NTAccount))
                    .Cast<FileSystemAccessRule>()
        let right = user.HasRightSafe(rule)
        where right.HasValue
        // Deny takes precedence over allow
        orderby right.Value == false descending
        select right;


  private static bool? HasRightSafe(this WindowsPrincipal user, FileSystemAccessRule rule)
  {
     try
     {
        return user.HasRight(rule);
     }
     catch
     {
        return null;
     }
  }

  private static bool? HasRight(this WindowsPrincipal user,FileSystemAccessRule rule )
     => rule switch
     {
        { FileSystemRights: FileSystemRights fileSystemRights } when (fileSystemRights &
                                                                      (FileSystemRights.WriteData | FileSystemRights.Write)) == 0 => null,
        { IdentityReference: { Value: string value } } when value.StartsWith("S-1-") &&
                                                            !user.IsInRole(new SecurityIdentifier(rule.IdentityReference.Value)) => null,
        { IdentityReference: { Value: string value } } when value.StartsWith("S-1-") == false &&
                                                            !user.IsInRole(rule.IdentityReference.Value) => null,
        { AccessControlType: AccessControlType.Deny } => false,
        { AccessControlType: AccessControlType.Allow } => true,
        _ => null
     };


  private static FileSystemSecurity GetFileSystemSecurity(this string filePath)
    => new FileInfo(filePath) switch
    {
       { Exists: true } fileInfo => fileInfo.GetAccessControl(),
       { Exists: false } fileInfo => (FileSystemSecurity)fileInfo.Directory.GetAccessControl(),
       _ => throw new Exception($"Check the file path, {filePath}: something's wrong with it.")
    };
}

-2
private static void GrantAccess(string file)
        {
            bool exists = System.IO.Directory.Exists(file);
            if (!exists)
            {
                DirectoryInfo di = System.IO.Directory.CreateDirectory(file);
                Console.WriteLine("The Folder is created Sucessfully");
            }
            else
            {
                Console.WriteLine("The Folder already exists");
            }
            DirectoryInfo dInfo = new DirectoryInfo(file);
            DirectorySecurity dSecurity = dInfo.GetAccessControl();
            dSecurity.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), FileSystemRights.FullControl, InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit, PropagationFlags.NoPropagateInherit, AccessControlType.Allow));
            dInfo.SetAccessControl(dSecurity);

        }

WellKnownSidType.WorldSid 是什么? - Kiquenet
这只是对你的问题@Kiquenet的随机回答。WorldSid是“所有人”建筑组。 - AussieALF

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接