В прошлом уроке мы рассмотрели статические права доступа, когда разработчик через серверный xml-файл настраивает разрешения для групп пользователей. Но платформа Workflow Technology предусматривает возможность использования в приложении динамических прав доступа. Таким образом, пользователи через интерфейс программы могут настраивать разрешения для групп.
Механизм динамических прав доступа будет отличным решением, когда пользователи могут самостоятельно редактировать список групп в программе.
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
Динамические права доступа
Настройка динамических прав доступа вынесена из серверной xml в базу данных.
Для этих целей существуют таблицы template.permission и template.group_permission. В первой мы должны перечислить имена всех Permission из серверного xml-файла, а во второй задать соответствие групп пользователей и доступных им Permission.
Описание Roles и Groups в серверном xml-файле теперь необязательно, но они не будут противоречить динамическим правам доступа. При совместном использовании статических и динамических прав доступа используется принцип разрешения. Иными словами, если в xml-файле у группы нет прав на выполнения какого-то запроса, а в таблице в базе данных такие права есть, то сервер будет разрешать пользователю выполнять этот запрос.
Подготовка Permission
Первым делом скорректируем набор Permission в серверном xml-файле: какие-то разрешения мы объединим, чтобы уменьшить их количество, а какие-то мы дополним необходимыми запросами. Таким образом, нам будет проще в дальнейшем настроить интерфейс для динамических прав.
Перейдем в файл серверной xml (Template.xml).
MaterialViewPermission
Скорректируем MaterialViewPermission, добавив запрос на получение списка единиц измерений:
Таким образом, в разрешении на просмотр списка ТМЦ мы будем сразу получать доступ к необходимому списку единиц измерений. При этом у группы пользователей может не быть доступа к UnitViewPermission.
OrderViewPermission
Скорректируем OrderViewPermission: перенесем необходимые запросы из OrderPaymentViewSqlQueryPermission, OrderPositionViewSqlQueryPermission, OrderPositionMaterialViewSqlQueryPermission, CityOrderViewSqlQueryPermission и ClientOrderViewSqlQueryPermission. А сами эти разрешения удалим.
Так же добавили запрос AccountSelectSqlQuery для оплат в заказе.
Помимо системных таблиц (template.permission и template.group_permission) создадим пару дополнительных таблиц, которые упростят нам работу с динамическими правами:
template.permission_block - будет описывать категории разрешений. Например, "Списки" или "Финансы";
template.permission_block_item - будет описывать группу разрешений, необходимых для чтения и редактирования списка сущности. Например, "Клиенты", "Заказы" или "Касса".
Выполним скрипт создания таблиц:
CREATESEQUENCEtemplate.permission_block_id_seq;CREATETABLEtemplate.permission_block( permission_block_id smallintNOT NULLDEFAULT nextval('template.permission_block_id_seq'::regclass), id_title character varying NOT NULL, title character varying NOT NULL, by_default booleanNOT NULLDEFAULT false,CONSTRAINT pk_permission_block_id PRIMARY KEY (permission_block_id),CONSTRAINT uniq_permission_block_name UNIQUE (id_title));CREATESEQUENCEtemplate.permission_block_item_id_seq;CREATETABLEtemplate.permission_block_item( permission_block_item_id smallintNOT NULLDEFAULT nextval('template.permission_block_item_id_seq'::regclass), permission_block_id smallintNOT NULL, id_title character varying NOT NULL, title character varying NOT NULL,CONSTRAINT pk_permission_block_item_id PRIMARY KEY (permission_block_item_id),CONSTRAINT fk_permission_block_id FOREIGN KEY (permission_block_id)REFERENCES template.permission_block (permission_block_id) MATCHSIMPLEONUPDATENOACTIONON DELETENOACTION,CONSTRAINT uniq_permission_block_item_name UNIQUE (id_title));
Обратите внимание, что в таблицах устанавливаем ограничение уникальности значений в полях id_title.
Внесем корректировку в таблицу template.permission:
Убедитесь, что вставляемые в таблицу template.permission имена разрешений совпадают с именами Permission в серверном xml-файле.
Таким образом, мы сформировали 7 основных блоков (категорий разрешений), в которых определили 15 групп, добавили имена всех разрешений из серверного файла и распределили их по группам.
В запросах мы создаем блок "Группы пользователей" и добавляем в него два разрешения GroupViewPermission и GroupEditPermission. Они пригодятся нам дальше, когда будем реализовывать возможность пользователям самим добавлять в программу новые группы пользователей.
У группы UserGroup в таблице template.group удалим значение из колонки name. Тем самым уберем группу из системных и дадим возможность настраивать ей права доступа через интерфейс программы. Можем для группы "Пользователи" изменить описание на "Настраиваемый доступ". Не забудьте удалить с колонки name ограничение NOT NULL.
Добавим группе "Администраторы" (AdministratorGroup) все права доступа, а группам "Пользователи" и "Гости" (GuestGroup) только общие права доступа - BaseViewPermission.
Вернемся в серверный xml-файл (Template.xml) и удалим тэги <Roles> и <Groups> - теперь будем работать только через динамические права доступа, чтобы не возникало путаницы с разрешениями прав доступа.
Добавим в Template.xml тэг <UserSettings> сразу после открывающего тэга <Workflow>:
Тем самым мы переведем серверную часть на работу с динамическими правами доступа и укажем, в каких таблицах находится необходимая информация.
Запустите приложение и проверьте загрузку главной формы для обеих групп пользователей.
Для группы "Пользователи" будут падать предупреждения, что у пользователя нет прав доступа к запросам CitySimpleSelectSqlQuery и ClientSimpleSelectSqlQuery. Чтобы исправить, вынесем их в новую команду AdditionalQueriesForOrdersDataConnectionRefreshCommand и добавим на нее условие OrderViewAccessPointCondition:
Добавим вызов новой команды во все места, где вызывается команда AllPrimaryGetDataConnectionRefreshCommand.
Список групп пользователей
Теперь можем заняться формами для редактирования списка групп пользователей. В этом разделе создадим форму списка и карточку для его редактирования, которая будет работать в двух режимах: редактирование наименования и описания для системных групп и настройка прав доступа для пользовательских групп:
Форма списка
Создайте форму для списка групп пользователей:
На главной форме (TemplateStart.xml) в меню добавьте пункт Администрирование -> Группы пользователей..., по которому будет открываться новая форма.
Скорректируем текст запроса для списка групп:
GroupSelectSqlQuery
Template.xml
<SqlQueryName="GroupSelectSqlQuery"> <Text> SELECT group_id AS "GroupId", title AS "Title", description AS "Description", archive AS "Archive", name IS NOT NULL AS "System" FROM template.group WHERE name IS DISTINCT FROM 'GuestGroup' ORDER BY title, group_id; </Text> </SqlQuery>
Колонка System необходима для того, чтобы ограничить возможность удалять и отправлять в архив системные группы. Создайте соответствующее условие и реализуйте эти ограничения. Значения из этой колонки будем передавать в одноименный параметр на форму редактирования группы.
Добавьте проверку результата команды архивирования группы пользователей. Переделайте проверку результата команды удаления.
Карточка группы
Перейдем в файл карточки группы (TemplateGroupEdit.xml) и добавим шрифт, который пригодится в таблице прав доступа:
Как говорилось ранее, карточка группы будет работать в двух режимах:
Полный доступ - для системных групп, которым права доступа задает разработчик. Таким группам пользователь сможет менять отображаемое имя и описание. Например, группа "Администраторы", у которой полный доступ ко всем данным и формам в приложении;
Настраиваемый доступ - для пользовательских групп, которым пользователь сам настраивает права доступа.
Запросы
Перейдем в файл Template.xml и скорректируем текст запроса GroupByIdSelectSqlQuery:
Template.xml
<SqlQueryName="GroupByIdSelectSqlQuery"> <Text> SELECT title AS "Title", description AS "Description" FROM template.group WHERE group_id = {GroupId}; </Text></SqlQuery>
Добавим запрос на получение дерева прав доступа с пометкой выбранных прав доступа для редактируемой группы:
Template.xml
<SqlQueryName="PermissionBlockItemSelectSqlQuery"> <Text> WITH group_permission_block_item_tmp AS ( SELECT DISTINCT PBI.permission_block_id, PBI.permission_block_item_id FROM template.group_permission GP JOIN template.permission P USING(permission_id) JOIN template.permission_block_item PBI USING(permission_block_item_id) WHERE group_id = {GroupId} ) SELECT ROW_NUMBER() OVER(ORDER BY permission_block_id) AS "PermissionBlockRowNumber", 0 AS "PermissionBlockItemRowNumber", PB.permission_block_id AS "PermissionBlockId", NULL::smallint AS "PermissionBlockItemId", PB.title AS "Title", COALESCE(array_compare(PBI.item_id_array, GPBI.item_id_array), False) AS "Checked" FROM template.permission_block PB LEFT JOIN LATERAL ( SELECT array_agg(permission_block_item_id) AS item_id_array FROM template.permission_block_item PBI WHERE PBI.permission_block_id = PB.permission_block_id ) AS PBI ON true LEFT JOIN LATERAL ( SELECT array_agg(permission_block_item_id) AS item_id_array FROM group_permission_block_item_tmp GPBI WHERE GPBI.permission_block_id = PB.permission_block_id ) AS GPBI ON true WHERE NOT PB.by_default UNION ALL SELECT DENSE_RANK() OVER(ORDER BY PB.permission_block_id) AS "PermissionBlockRowNumber", ROW_NUMBER() OVER(PARTITION BY PB.permission_block_id ORDER BY PBI.permission_block_item_id) AS "PermissionBlockItemRowNumber",
PBI.permission_block_id, PBI.permission_block_item_id, ' ' || PBI.title, GPBI.permission_block_item_id IS NOT NULL AS "Checked" FROM template.permission_block PB JOIN template.permission_block_item PBI USING(permission_block_id) LEFT JOIN group_permission_block_item_tmp GPBI USING(permission_block_item_id) WHERE NOT PB.by_default ORDER BY "PermissionBlockRowNumber", "PermissionBlockItemRowNumber"; </Text></SqlQuery>
Создайте на форме объекты TitleTextBox (Наименование), DescriptionTextBox (Описание) и PermissionPanel, в которой будет располагаться таблица PermissionDatabaseTable с заголовком PermissionLabel (Настраиваемые права доступа):
Запустите приложение и проверьте отображение объектов формы:
Так же проверьте отображение списка возможных прав доступа:
Скорректируем логику выбора прав доступа в таблице, чтобы было проще работать с таблицей. Реализуем два момента:
Когда ставим галочку в строке с заголовком блока (Например, по строке "Отчеты"), то галочки должны проставиться во все строки этого блока. И наоборот, если галочку снимаем с заголовка блока, то должны сняться галочки со всех строк блока;
Когда поставили галочки на всех строках блока, то автоматически должна проставиться галочка на заголовке блоке. Если сняли галочку хотя бы с одной строки блока, то должны снять галочку и с заголовка блока.
Добавим условие проверки, что в выбранной строке в колонке PermissionBlockItemId пустое значение, то есть пользователь кликнул по строке заголовка блока:
С особенностями универсального значения <Array> мы уже немного знакомы. В разделе "Дополнительно" из блока "Основной" в статье Array разбирали несколько примеров. Если Вы пропустили эту статью, советуем сначала прочитать ее, а затем вернуться к уроку и продолжить выполнение задания.
Ниже подробно рассмотрим <Array> из нового условия.
Из таблицы PermissionDatabaseTable с помощью get-проперти DictionaryArrayData получаем словарь массивов со значениями каждой колонки, который передаем в тэг <Source> в качестве источника для <Array>.
Из полученного словаря нам интересны только три колонки (PermissionBlockId, PermissionBlockItemId и Checked), значения которых достаем в тэге <Select> по имении формируем матрицу значений.
Затем фильтруем матрицу и оставляем только те строки, у которых значение 0-го элемента (колонка PermissionBlockId) совпадает со значением в колонке PermissionBlockId выделенной строки и значение 1-го элемента (колонка PermissionBlockItemId) не является пустым.
Иными словами, мы получаем из таблицы строки из одного блока с выделенной строкой и исключаем строку с именем самого блока.
С помощью тэга <Distinct> мы формируем массив уникальных значений по 2-ому элементу (колонка Checked).
<Distinct> <OnIndex="2" /></Distinct>
После с помощью тэга <Count> считаем их количество, которое передается в условие и сравнивается с единицей.
<Count />
Если все строки имеют одинаковое значение в колонке Checked, то тэг <Count> вернет значение 1, и значение условия PermissionCheckedAllCondition будет True.
Теперь создадим команду, которая будет всем строкам блока в колонку Checked проставлять значение выделенной строки:
Поймав событие изменения значения в колонке Checked таблицы PermissionDatabaseTable, первым делом проверяем, был ли клик по строке заголовка блока. Если выделенная строка является заголовком блока, то всем строкам блока проставим ее значение. Если выделенная строка не является заголовком блока, то проверяем значения в колонке Checked для всех строк блока. Если эти значения одинаковые, то и заголовку блока проставим такое же значение, иначе снимем галочку с заголовка блока.
Запустите приложение и проверьте работу формы TemplateGroupEdit.xml.
Сохранение прав доступа
Скорректируем сохранение изменений для прав доступа.
Чтобы извлечь из таблицы массив идентификаторов разрешений (PermissionBlockItemId), которые пользователь выбрал для группы, воспользуемся get-проперти FilteredColumnValues объекта DatabaseTable:
<ObjectName="PermissionDatabaseTable"> <PropertyName="FilteredColumnValues"> <Parameters> <ParameterName="ColumnName">PermissionBlockItemId</Parameter> <ParameterName="Filter">Checked AND PermissionBlockItemId IS NOT NULL</Parameter> </Parameters> </Property></Object>
В параметр ColumnName передаем имя колонки, из которой хотим получить значения. А в параметр Filter укажем условие выборки строк: в колонке Checked должно стоять значение true, и значение в колонке PermissionBlockItemId не должно быть пустым.
Эту конструкцию укажем в качестве значения нового параметра для GroupInsertSetDataConnection и GroupUpdateSetDataConnection:
<ParameterNativeName="PermissionBlockItemId"SendAsArray="True"> <Value> <ObjectName="PermissionDatabaseTable"> <PropertyName="FilteredColumnValues"> <Parameters> <ParameterName="ColumnName">PermissionBlockItemId</Parameter> <ParameterName="Filter">Checked AND PermissionBlockItemId IS NOT NULL</Parameter> </Parameters> </Property> </Object> </Value></Parameter>
Обратите внимание, что у тэга <Parameter> появился атрибут SendAsArray со значением True, таким образом, значение параметра будет передаваться как массив, и в запросе вместо переменной будет подставляться конструкция ARRAY[..] с переданными значениями. В противном случае на сервер передавался бы первый элемент массива.
Скорректируем запросы сохранения изменений:
Template.xml
<SqlQueryName="GroupInsertSqlQuery"> <Text> INSERT INTO template.group ( title, description ) VALUES ( {Title}, {Description} ) RETURNING group_id; -- Добавление разрешений INSERT INTO template.group_permission(group_id, permission_id) SELECT currval('template.group_id_seq'::regclass), permission_id FROM template.permission P JOIN template.permission_block_item PBI USING(permission_block_item_id) JOIN template.permission_block PB USING(permission_block_id) WHERE PB.by_default OR P.permission_block_item_id = ANY ({PermissionBlockItemId}::smallint[]); </Text></SqlQuery>
Для всех новых групп сразу добавляем разрешение по умолчанию (by_default).
Template.xml
<SqlQueryName="GroupUpdateSqlQuery"> <Text> UPDATE template.group SET title = {Title}, description = {Description} WHERE group_id = {GroupId}; -- Добавление разрешений INSERT INTO template.group_permission(group_id, permission_id) SELECT {GroupId}, permission_id FROM template.permission P WHERE P.permission_block_item_id = ANY ({PermissionBlockItemId}::smallint[]) ON CONFLICT (group_id, permission_id) DO NOTHING; -- Удаление разрешений DELETE FROM template.group_permission GP USING template.permission P JOIN template.permission_block_item PBI USING(permission_block_item_id) JOIN template.permission_block PB USING(permission_block_id) WHERE GP.permission_id = P.permission_id AND group_id = {GroupId} AND NOT PB.by_default AND P.permission_block_item_id != ALL ({PermissionBlockItemId}::smallint[]); </Text></SqlQuery>
Так как на таблице template.group_permission стоит ограничение уникальности пары group_id и permission_id, то в INSERT-запрос добавлена инструкция ON CONFLICT (group_id, permission_id) DO NOTHING. Таким образом, при нарушении ограничения уникальности PostgreSQL не будет предпринимать никаких действий.
Подробнее про предложение ON CONFLICT можно почитать в официальном справочнике PostgreSQL по ссылке.
Запустите приложение и настройте права доступа для группы "Пользователи". Создайте новую группу, чтобы проверить работу запроса на вставку данных.
Самостоятельно
Все Permission на работу с заказами мы объединили в одно разрешение "Заказы". Но можно разделить их на два: одно разрешение будет на создание и редактирование заказов, а второе на добавление и редактирование оплат в заказе. С помощью такого разделения можно гибко настроить права доступа к заказам. Например, группа "Пользователи" может создавать заказы и добавлять в них оплаты, а группа "Бухгалтеры" может только просматривать карточку заказа (без редактирования) и редактировать оплаты в заказе.
Попробуйте реализовать такой сценарий в проекте. При этом учтите, что поля с данными по заказу и таблица с позициями заказа должны быть недоступны для редактирования, если у пользователя нет соответствующих прав доступа.
В ответах к уроку нет решения данной задачи, так как она не влияет на работу приложения в рамках обучающего курса. Но решение ее позволит Вам лучше понять разделение прав доступа между группами пользователей.
Итоги
В этом уроке мы рассмотрели правила организации динамических прав доступа и реализовали их в нашем проекте.
Ответы
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.