- Создание и изменение в Powershell NTFS разрешений ACL
- Основы разрешений
- Получение текущих разрешений в Powershell
- Поиск всех папок с правами у определенной группы
- Изменение, копирование и добавление разрешений
- Изменение и добавление прав у пользователя и групп
- Удаление прав у пользователя или группы
- Смена владельца
- Включение или отключение наследования папок и файлов
- Сбор всех прав и их экспорт в CSV
- ACL: в поисках идеального решения
- Эволюция системы разделения прав
- Как надо было делать
- Реализация
- Базовый интерфейс (Базовый модуль)
- Модуль ролей
- Модуль атрибутов
- Хранение в базе
- Задание начальных значений
- Интерфейс редактирования прав
- Форма создания/редактирования прав
- Синхронизация
- Сбор данных из кода
- Сбор ресурсов
- Сбор атрибутов
- Сбор действий
- Из кода в базу
- Формат файла
- Псевдо -группы, -ресурсы
- Последний штрих
- То что не попало в статью
- Поля (параметры, свойства) моделей как атрибуты
- Разделение прав на уровне полей модели
Создание и изменение в Powershell NTFS разрешений ACL
Основной способ ограничения доступа к файлам и папкам дает файловая система NTFS с ее таблицами ACL. Это может быть право только на чтение файла (открытие и просмотр), на чтение и запись (открытие, просмотр, изменение и сохранение) и многие другие. Такие права мы чаще устанавливаем через GUI назначая права не конечному пользователю, а группе в которой он состоит. Все действия по созданию и изменению мы так же можем сделать через Powershell.
Навигация по посту
На мой взгляд использование консоли Powershell лишнее при выдаче таких прав. Риск совершить ошибку в консоли намного выше, чем при работе в GUI, да и время для написания команды уйдет больше. Есть специфичные задачи, например в виде аудита или перенос прав где Powershell очень поможет.
Основы разрешений
ACL (access controll list) — делится на два вида:
- SACL (System Access Control List) — используется для аудита;
- DACL (Discretionary Access Control List) — используется для выдачи и проверки разрешений пользователям и группам.
Оба этих типа разрешений хранятся в специальной таблице MFT (Master File Table).
Основное средство для редактирования этих разрешений в GUI можно увидеть зайдя в свойства файла или папки:
В области 4 выделены следующие разрешения:
- Read — открытие файла и папки;
- List folder contents — открытие папки;
- Write — создание файлов и папок и их изменение;
- Read & Execute — открытие и запуск исполняемых файлов;
- Modify — открытие, создание, изменение и удаление файлов и папок;
- Full Control — включает разрешения modify, а так же управление разрешениями файла или папки.
Чаще всего мы работаем с разрешениями выше, но есть еще один список с возможностью настройки прав более тонко:
Как можно догадаться — разрешения указанные в области 1 это просто набор нескольких правил из области 3. Их так же еще называют «Premission Sets» и «Special Premissions».
Групповые разрешения могут принимать флаги Allow и Deny, которые разрешат или запретят указанные действия. Указывать разрешения для пользователей через Deny считается плохой практикой и практически не используется.
Кроме этого существует наследование:
Наследование помогает установить разрешения для одной папки так, что оно будет применяться ко вложенным файлам и папкам. Если наследование отключить (2), то у нас будет возможность убрать все наследуемые разрешения или оставить их.
Функции по работе со строками в Powershell
Получение текущих разрешений в Powershell
На примере ниже я верну разрешения для папки «C:\TestFolder\»
В области 1 выделен владелец папки, а под областью 2 отображаются все группы и пользователи с правами.
Мы можем проверять права не только локальной, но и сетевой папки. На примере ниже возвращена та же папка:
Поиск всех папок с правами у определенной группы
Представим, что мы хотим проверить права данные определенной группе. Мы можем заходить в свойства каждой папки и смотреть вкладку «Безопасности», а можем сделать это через Powershell.
Обычно у нас есть какой-то каталог с общим доступом с папками внутри, на которые мы выдаем разрешения. В моем случае такой каталог «Moscow», а на папки внутри я уже даю права:
В следующем примере я узнаю на какие папки установлены разрешения для TestGroup:
Изменение, копирование и добавление разрешений
Еще одна причина использовать Powershell — это возможность копирования разрешений. Например так я поставлю разрешения для папки Folder2 такие же, как и Folder2:
Возможности добавить нового пользователя в список ACL или изменить его права одним командлетом у нас нет. В обоих случаях мы должны будет создавать копию объекта ACL, изменять ее отдельным классом, а затем применять используя метод. Сам объект, который мы получаем через Get-ACL, имеет множество методов:
Для создания нового объекта с правами используется класс «FileSystemAccessRule», который в команде будет выглядеть так:
Расшифровать значения можно следующим образом:
- IdentityReference\String — пользователь или группа формата «DOMAIN\Administrator»;
- FileSystemRights — сами разрешения, например «Read»;
- InheritanceFlags и PropagationFlags — определяют наследование. Например вы можете сделать так, что папки внутри указанной будут наследовать разрешения, а файлы нет. Ниже будут приведены несколько примеров. Более подробно об этом можно почитать на сайте Microsoft;
- AccessCpmtrolType — разрешить или запретить (Allow/Deny).
Изменение и добавление прав у пользователя и групп
Допустим у нас есть пользователь «Test User (001)» с возможностью чтения папки «Folder1» и мы хотим добавить еще права на запись. Это будет выглядеть так:
Наследование типа ‘ContainerInherit, ObjectInherit’ говорит о том, что оно касается этой папки и всех вложенных папок и файлов.
Таким же образом работает добавление новой группы или пользователя в список ACL:
Права, которые мы можем дать имеют сокращенное название и они отображены далее:
У вас может быть много ошибок связанных с созданием класса с новыми разрешениями. Я бы советовал выводить существующие права и сравнивал бы их с новыми — это позволит снизить вероятность ошибок.
При этом вы можете применить набор разрешений, например в виде Write, но результат будет отображаться в виде «Special Premission»:
Если бы я указал наследование в виде ‘ContainerInherit, ObjectInherit’, то права бы применились как нужно:
Поэтому я рекомендую смотреть существующие разрешения на примере того, как я сделал это выше.
Удаление прав у пользователя или группы
Для удаления всех разрешений есть метод «RemoveAccessRuleAll». Работает он так же:
Для удаления конкретного права, например только возможность чтения, есть метод «RemoveAccessRule». С этим методом у меня были проблемы после которых действующие разрешения менялись на Special и изменить эту ситуацию у меня не получилось. Если вам нужно все же убрать одно разрешение — я бы советовал удалять все разрешения у пользователя, а затем добавлять их заново. У пользователя было право на Чтение и Запись, но мне нужно было оставить только чтение:
Смена владельца
Смена владельца файла или папки делается через метод SetOwner. Этот метод, в качестве идентификатора, принимает SID пользователя и что бы его узнать нужно использовать класс «System.Security.Principal.Ntaccount». На практике это выглядит так:
Как отсортировать в Powershell объекты через Sort-Object
Включение или отключение наследования папок и файлов
Для управления наследованием используется метод «SetAccessRuleProtection», который устанавливает следующее:
- Нужно ли блокирование от родительской папки.
- Нужна ли перезапись прав.
Значения указываются в $True или $False. Например так я включу наследование и заменю существующие разрешения родительскими:
Сбор всех прав и их экспорт в CSV
Учитывая примеры выше мы можем временно собирать отчеты по выданным правам. Пример того, как может выглядеть отчет:
ACL: в поисках идеального решения
Новый проект. В очередной раз пришлось решать проблему с разграничением прав. В очередной раз пришлось изобретать велосипед. Вот я и подумал, а не проще ли разобраться с этой проблемой раз и навсегда. Хочу решить задачу «на бумаге», чтобы эти принципы можно было использовать независимо от технологии.
Эволюция системы разделения прав
Обычно разделение прав эволюционирует так:
Сначала делают флаг admin в таблице user.
Потом оказывается, что кроме админов бывают и другие типы пользователей. Добавляют группы (предопределенный набор). Права и группы жестко связаны. Соотношение между группами и пользователями один ко многим. Так часто бывает потому, что разделение прав связывают с организационной иерархией.
Дальше конечные пользователи хотят сами создавать группы и раздавать им права.
Следующая ступень — некоторым пользователям нужны права, выходящие за рамки их группы. Тут есть такие варианты:
- Доработать систему, чтобы можно было выдавать права напрямую пользователю. Если база реляционная, то получаться запросы с union.
- Можно делать группы под каждого пользователя с «экстра» правами. При таком подходе будет проблема с массовым редактированием. Допустим, у нас есть пользователи, которые имеют права A и B и некоторое количество пользователей (из той же организационной группы), но у которых есть ещё и другие права X, Y, Z. В один момент понадобилось убрать у всей группы право A. Для этого надо будет убирать право A из каждой специально созданной «экстра» группы.
Для решения проблемы с «экстра» правами иногда делают наследование групп. При этом уходит проблема с массовым редактированием. Но появляется проблема разрешением конфликтов разрешений/запретов. Например: в группе A какое-то действие разрешено, а в группе B, которая от неё наследуется, это же действие запрещено.
С технической точки зрения есть неудобный момент — работа с «древовидной» структурой в реляционной базе. Например, для получения списка прав: сначала придётся получить все родительские группы, потом нужно будет сделать join на таблицу прав. После получения всего списка из базы к нему нужно будет применить правила разрешения конфликтов. В MySQl иногда используют для этого «хак» c GROUP BY и ORDER, но это решение не портабельное, так как такое поведение GROUP BY не соответствует спецификации SQL.
Конечно, это решаемые проблемы. Можно придумать, как хранить роли, как разрешать конфликты. Но будет ли легко конечным пользователям разобраться в этой логике?
Так же скорее всего придётся столкнуться с проблемой «синхронизации» между кодом и базой. Чтобы права были редактируемые — их нужно хранить в базе. Но функции, которые отвечают за исполнение разделения прав, хранятся в коде.
Как надо было делать
Уже видя, что из этого может получиться, можно сделать следующие выводы:
Сразу писать систему разделения прав с расчётом на более чем две группы (админы и не админы).
Следующая тонкость — в моем описании есть небольшая подмена понятий. Группа (которая отражает принадлежность к иерархической структуре в реальном мире) и группа (роль) в системе разделения прав не всегда одно и тоже. Специально оставил, так как сам наступал на эти грабли (думаю, не я один). Нужно разделить понятия группа и роль. Тогда можно будет сделать соотношение между ролями и пользователями многие ко многим.
При соотношении многие ко многим уходит проблема с «экстра» правами и наследованием групп. Экстра права выносятся в отдельные роли, которые добавляются пользователю, кроме «основной» роли.
Решение конфликтов тоже упрощается: так как структура прав «плоская», то и сами права будут простые. Например, могут быть такие правила:
- по умолчанию все запрещено;
- можно выдавать разрешения и запреты;
- запрет «перевешивает» любое количество разрешений;
- правила разрешений складываются (логическое или);
- правила запретов складываются (логическое или);
- потом из всех прав разрешений вычитаются все права запретов, а все, что осталось — и есть искомое множество прав, которые разрешены пользователю.
Реализация
Опишу реализацию на примере веб приложения, которое следует MVC паттерну. В таком приложении права можно разграничивать на уровне контроллера и/или на уровне модели. Так же ACL должен предоставлять хелперы для представления (view).
Базовый интерфейс (Базовый модуль)
Необходимый минимум для работы ACL.
Cамая базовая функция, которая должна быть в любом ACL, отвечающая на вопрос, есть ли у данного пользователя право на данное действие с данным ресурсом:
Функция для вызова в контроллере _ для проверки, есть ли доступ у текущего пользователя. При этом предполагается, что контроллер предоставляет метод для получения текущего пользователя (CoC).
В случае, если доступа нет, то будет передано управление обработчику ошибок неавторизованного доступа. В зависимости от ЯП это можно сделать через исключения или через колбеки.
Должна быть возможность задавать свой способ обработки ошибок не авторизованного доступа. Например, если используется исключения, то в базовом классе контроллера можно задать обработчик так:
Должна быть возможность задать проверку для всех контроллеров разом. Чтобы не надо было в каждом контроллере ставить вызов authorize. В зависимости от технологии это можно сделать или в базовом классе контроллера или с помощью программной прослойки (middleware).
Для базового интерфейса не принципиально устройство групп. Это сделано специально, чтобы его можно было легко внедрять в существующие проекты с целью переделывать по кускам. Базовый интерфейс даже сможет работать с флагом admin в таблице user. Для этого нужно будет реализовать кусок кода, который отвечает за определение прав по пользователю. Например:
Модуль ролей
Предполагается, что модель пользователя предоставляет метод для получения ролей пользователя (CoC).
Если группы не нужно хранить в базе, то задание прав происходит с помощью DSL.
Модуль атрибутов
Рассмотрим пример: автор может делать все CRUD операции со статьями. Но редактировать, удалять статьи, а также просматривать черновики он может только свои. Т.е. получается, что теперь не все статьи одинаковы. Такое разделение прав решается введением атрибутов для ресурсов.
Первый параметр — черновик или нет (draft=true/false); второй — принадлежит ли автору (own).
Итого:
Для поддержки таких атрибутов нужно будет реализовать вспомогательные функции (helpers). Для работы с инстансом модели (ресурсом):
Для работы с методом получения данных (списка) из базы.
Для моделей нужно будет реализовать метод, который будет вызывать необходимые функции. Использование этого метода может выглядеть так:
При вызове этой функции произойдет обращение к модулю разделения прав. Если задать пользователя на уровне модуля разделения прав, то не нужно будет указывать пользователя в каждом вызове этого метода.
К ресурсу можно применить более одного атрибута, при этом условия атрибутов должны складывать по правилам логического и.
Хранение в базе
Описанная структура легко сохраняется в базу.
Пользователь, Роль — n к n; Роль, Право — n к n; Право, Ресурс — n к 1; Право, Атрибут — n к n; Право, Действие — n к 1;
Задание начальных значений
В системе должны быть настройки по умолчанию. Чтобы не зависеть от реализации базы для задания начальных значений можно использовать все тот же DSL (функция allow).
Интерфейс редактирования прав
Под редактированием понимается возможность создавать, удалять роли и возможность редактировать права роли. Для создания права можно будет выбрать ресурс, действие и атрибуты из предопределённого набора. Чтобы с этим мог работать конечный пользователь они должны иметь человеко-понятный вид (а не вид костант, которые используются в коде).
Соответственно в базе нужно хранить этот предопределённый набор и человеко-понятное обозначение констант.
Форма создания/редактирования прав
Должны быть такие поля:
- роль (выбор или заданная в контексте)
- выбор ресурса
- в зависимости от выбранного ресурса можно выбрать действие
- в зависимости от выбранного ресурса можно выбрать атрибут
- allow/deny
А отображаться права будут так:
Синхронизация
Задания для поддержки синхронности между кодом и базой, а так же для проверки целостности лучше сразу автоматизировать (написать скрипты), так как их придётся выполнять много раз.
Сбор данных из кода
Сначала надо собрать все данные по коду.
Сбор ресурсов
Ресурсами могут выступать модели или контроллеры (они же маршруты или route). Модели легко собрать, по идеологии MVC они должны находиться отдельно (в отдельной папке). При сборе маршрутов тоже не должно возникнуть проблем. В современных MVC фреймворках задание маршрута выглядит примерно так:
Естественно можно собирать оба вида ресурсов или только один в зависимости от потребностей.
Сбор атрибутов
Это тоже довольно легко сделать, ведь атрибуты — это функции определённого вида, лежащие в определённом месте. Также для атрибутов надо будет хранить информацию о том, для каких моделей они подходят.
Сбор действий
Есть 4 стандартных действия для моделей (CRUD) и одно действие для маршрута (access). Все остальные действия можно искать по коду текстовым поиском (по функции allow, authorize итп).
Из кода в базу
После сбора данных по коду нужно сложить их в промежуточное хранилище (файл). Тогда в этот же файл можно будет добавлять человекопонятные описания. Так же это позволить обойти несовершенство алгоритма сбора (поиска) данных по коду. Допустим, алгоритм не находит некоторые данные в коде (скорее всего действия). Тогда их можно будет дописать в файл «руками». При следующем запуске алгоритм опять не найдёт эти же данные и удалит их из файла. Чтобы этого не происходило, их надо пометить, как добавленные вручную, чтобы скрипт синхронизации не пытался их удалить. Получившийся файл надо хранить в системе контроля версий вместе с кодом.
При доставке обновлений или при разворачивании системы на другом железе (deployment) нужно будет запустить скрипт синхронизации. Скрипт считает данные из файла. Сделает проверку, что нет конфликтов — не удалены данные, которые ещё участвуют в разделении прав. После чего запишет новые данные в базу. Дальше добавит новые права по умолчанию (если есть еще один файл с правами по умолчанию).
Формат файла
При автоматической сборке данных в файл могут попадать «лишние» данные, которые не нужно отображать в интерфейсе редактирования (не нужно добавлять в базу). Для таких данных нужно ввести признак того, что их не нужно сохранять в базу (например, значение false вместо описания).
Псевдо -группы, -ресурсы
Для удобства можно создать такие псведо-группы:
- all — все пользователи
- authenticated — все залогиненные пользователи
- non_authenticated — все не залогиненные пользователи
И такой псевдо-ресурс:
- all — все ресурсы
Последний штрих
Надо не забыть добавить хелперы для тестирования.
То что не попало в статью
Поля (параметры, свойства) моделей как атрибуты
Изначально я хотел собирать не только функции (хелперы) атрибутов, но и все поля моделей, чтобы можно было с ними сравнивать (draft=false, amount>100). Но понял, что это не очень хорошая идея и вот почему:
- их будет очень много, и большинство из них не будут принимать участия в разделении прав, т.е. будет очень много мусора
- если разрешить задавать сложные зависимости, то может получиться, что бизнес логика перекочует из кода в базу.
Поэтому я решил отказаться от идеи задания значений через поля моделей. Если нужно задать такое значение – то придется написать хелперы для атрибутов, т.е. для draft=true/false нужно делать так:
Разделение прав на уровне полей модели
Решение пока только «на бумаге». Во время реализации могут найтись подводные камни или идеи получше. Публикую статью, чтобы хабрачитатели указали на недочёты в моем подходе и предложили свои идеи.
UPD
Начал отвечать на комментарии, думать над реализацией и сразу нашел ошибки:
Должна быть возможность задать проверку для всех контроллеров разом. Чтобы не надо было в каждом контроллере ставить вызов authorize. В зависимости от технологии это можно сделать или в базовом классе контроллера или с помощью программной прослойки (middleware).