Перевірка дозволів на записування каталогів та файлів у .NET


80

У моїй програмі .NET 2.0 мені потрібно перевірити, чи існує достатньо дозволів на створення та запис у файли в каталог. З цією метою у мене є наступна функція, яка намагається створити файл і записати в нього один байт, а потім видалити себе, щоб перевірити, чи існують дозволи.

Я зрозумів, що найкращим способом перевірки було насправді спробувати це зробити, вловлюючи будь-які винятки, що трапляються. Однак я не особливо задоволений загальним винятком, тому чи є кращий чи, можливо, більш прийнятний спосіб зробити це?

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;Будь ласка, використовуйте метод Path.Combine замість об’єднання рядків, щоб отримати fullPath. Path.Combine(directory, TEMP_FILE)
nomail

Що робити, якщо хтось вдарить, а потім наступного дня. Що робити, якщо вони вдарять, а потім вибивають через два дні? Я впевнений, що люди не повинні робити ці речі, але поведінку слід визначити.
Скотт Ханнен,

Відповіді:


23

Відповіді Річарда та Джейсона начебто в правильному напрямку. Однак те, що ви повинні робити, - це обчислення ефективних дозволів для ідентифікатора користувача, на якому запущений ваш код. Наприклад, жоден із наведених прикладів не враховує належне членство в групі.

Я впевнений , що Кіт Браун мав деякий код , щоб зробити це в своїй вики - версії ( в автономному режимі в цей час) з The .NET Керівництво для розробників в безпеки Windows . Це також досить докладно обговорюється в його книзі " Програмування безпеки Windows" .

Обчислення ефективних дозволів не для людей зі слабким серцем, і ваш код намагається створити файл та вловити виняток безпеки, який викидається, мабуть, шлях найменшого опору.


2
Це також єдиний надійний метод, оскільки інакше хтось міг би змінити дозвіл між перевіркою та фактичною спробою збереження (малоймовірно, але можливо).
Кріс Чілверс,

1
Дякую за це. Отже, єдина зміна, яку я повинен зробити у своєму коді, - це вловити виняток безпеки, а не загальний „Виняток”?
Енді,

@Andy - так, це шлях найменшого опору, якщо ви не хочете писати код для обчислення ефективних дозволів.
Кев

2
чому все повинно бути так складно!
Vidar

3
@Triynko - Я пропоную вам прочитати статтю, яку я процитував: groups.google.com/group/… - обчислення ефективних дозволів не так просто, як це звучить. Будь моїм гостем і дай відповідь, щоб довести, що я не правий.
Kev

49

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 використовує щось, що називається "Прапори", крім того, якщо ви не знаєте, що це, вам слід справді прочитати :)


6
Звичайно, це призведе до винятку, якщо ви насправді не можете отримати ACL у каталозі.
blowdart

3
Що це перевіряє? Цей каталог має дозволи на запис, але для якого користувача? :)
Іван Г.

2
Це працює, якщо ви просто хочете побачити, чи має поточний користувач доступ до запису.
Донні В.

@aloneguid: Методи "GetAccessRules" повертають AuthorizationRuleCollection. Клас AthorizationRule має властивість IdentityReference, тип виконання якого насправді буде одним із двох, що походять від типу IdenityReference (NTAccount або Security), який, як ви бачите, вказаний у виклику GetAccessRules. За допомогою екземпляра IdentityReference (або його похідних типів) ви можете виявити, до якого користувача застосовується правило. Він буде у формі SID або імені NTA-рахунку.
Трійко

7
Спробуйте запустити це на системному диску в Windows 7 із додатком, що не є адміністратором, і це поверне істину, але при спробі написати в c: \ ви отримаєте виняток, в якому зазначено, що у вас немає доступу!
Пітер

34

Denyмає перевагу над Allow. Місцеві правила мають перевагу над успадкованими правилами. Я бачив багато рішень (включаючи деякі відповіді, показані тут), але жодне з них не враховує, успадковуються правила чи ні. Тому я пропоную наступний підхід, який розглядає успадкування правил (акуратно упакований у клас):

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
Я думаю, що ця відповідь є найкращим способом її досягнення, інші відповіді також використовують той самий спосіб, щоб отримати результат, але оскільки лише ця відповідь обчислює успадковані правила та місцеві правила, вона, на мою думку, є найбільш точною. Дякую та вітаю.
Толга Евцімен

19

Прийнята відповідь Кев на це питання насправді не дає жодного коду, вона просто вказує на інші ресурси, до яких я не маю доступу. Тож ось моя найкраща спроба функції. Він фактично перевіряє, чи дозвол, який він переглядає, є дозволом "Написати" і чи поточний користувач належить до відповідної групи.

Це може бути не повним щодо мережевих шляхів чи чогось іншого, але це досить добре для моєї мети, перевіряючи локальні конфігураційні файли в розділі "Програмні файли" на наявність запису:

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;
    }
}

Цей не працює для груп, але для буквально доданих імен облікових записів лише в моєму випадку
випадковий

Тож це щось пов’язане із форматом типу "(S-1-5-21-397955417-626881126-188441444-512)"? Чи перетворило рядок на такий як SecurityIdentifier, вирішило вашу проблему? З вашого коментаря не зрозуміло, працює це зараз для вас чи ні.
Брайс Вагнер,

Коли ви ставите "rule.IdentityReference.Value" як параметр currentuser.IsInRole (), ви використовуєте метод IsInRole (рядок), який намагається збігатися за звичайним значенням "домен \ користувач". Отже, ви натискаєте рядок SID замість рядка імені користувача. Однак якщо ви використовуєте мій рядок перед цим, ви отримаєте об'єкт SecurityIdentifier, який відповідає користувачеві даного SID. Це перевантаження аргументу "рядок" є невеликою пасткою для розробників, і знову він приймає ім'я облікового запису або групи у форматі, що підлягає переказуванню, а не у вигляді SID рядка.
Випадковий

Проблема в тому, що "новий SecurityIdentifier (SDDLFormat)" не працює із звичайними іменами груп (ви отримуєте виняток з аргументу). Тож я додав перевірку, чи це у форматі SDDL.
Брайс Вагнер,

2
Це рішення працювало для мене, але мала одну проблему з мережевою папкою. У папці є правило доступу, що дозволяє писати в BUILTIN\Administrators. І оскільки я адміністратор на своїй місцевій станції, фрагмент помилково повернувся true.
Ilia Barahovski

5

IMO, вам потрібно працювати з такими каталогами, як зазвичай, але замість перевірки дозволів перед використанням надайте правильний спосіб обробки UnauthorizedAccessException та реагуйте відповідно. Цей метод простіший і набагато менше схильний до помилок.


1
Ви, мабуть, хотіли сказати: "Цей метод простіший і набагато менше схильний до помилок".
cjbarth,

3

Спробуйте працювати з цим фрагментом 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. docs.microsoft.com/en-us/dotnet/csharp/language-reference/... Виклику до GetAccessRules()потрібен тип облікового запису при виклику . msdn.microsoft.com/en-us/library/…
Джейсон Еванс,

Навіщо використовувати NTAccount? завжди користуватися NTAccount ?
Кікенет,

У цьому випадку так. NTAccountпредставляє обліковий запис користувача на ПК з Windows, саме тому він нам потрібен у наведеному вище коді.
Джейсон Еванс,

1

Оскільки статичний метод 'GetAccessControl', здається, відсутній у поточній версії .Net core / Standard, мені довелося змінити відповідь @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.")
    };
}

0

за цим посиланням: 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
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 WorldSid є група "Усі".
AussieALF
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.