Перевірка дозволів на записування каталогів та файлів у .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.