Урок 19. Динамические права доступа

В прошлом уроке мы рассмотрели статические права доступа, когда разработчик через серверный 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, добавив запрос на получение списка единиц измерений:

Template.xml
<Permission Name="MaterialViewPermission">
  <AccessPoint Name="MaterialViewAccessPoint" />
  <SqlQuery Name="MaterialSelectSqlQuery" />
  <SqlQuery Name="MaterialByIdSelectSqlQuery" />
  <!-- Дополнительно -->
  <SqlQuery Name="UnitSelectSqlQuery" />
</Permission>

Таким образом, в разрешении на просмотр списка ТМЦ мы будем сразу получать доступ к необходимому списку единиц измерений. При этом у группы пользователей может не быть доступа к UnitViewPermission.

OrderViewPermission

Скорректируем OrderViewPermission: перенесем необходимые запросы из OrderPaymentViewSqlQueryPermission, OrderPositionViewSqlQueryPermission, OrderPositionMaterialViewSqlQueryPermission, CityOrderViewSqlQueryPermission и ClientOrderViewSqlQueryPermission. А сами эти разрешения удалим.

Так же добавили запрос AccountSelectSqlQuery для оплат в заказе.

Итоговый синтаксис разрешения будет иметь вид:

Template.xml
<Permission Name="OrderViewPermission">
  <AccessPoint Name="OrderViewAccessPoint" />
  <SqlQuery Name="OrderSelectSqlQuery" />
  <SqlQuery Name="OrderByIdSelectSqlQuery" />

  <SqlQuery Name="OrderPaymentByOrderIdSelectSqlQuery" />
  <SqlQuery Name="OrderPaymentByIdSelectSqlQuery" />
  <SqlQuery Name="OrderPositionByOrderIdSelectSqlQuery" />
  <SqlQuery Name="OrderPositionByIdSelectSqlQuery" />
  <!-- Дополнительно -->
  <SqlQuery Name="AccountSelectSqlQuery" />
  <SqlQuery Name="ClientShortSelectSqlQuery" />
  <SqlQuery Name="ClientSimpleSelectSqlQuery" />
  <SqlQuery Name="CitySimpleSelectSqlQuery" />
  <SqlQuery Name="MaterialSimpleSelectSqlQuery" />
  <SqlQuery Name="MaterialShortSelectSqlQuery" />
</Permission>

OrderEditPermission

Подобным образом скорректируем и OrderEditPermission, объединив с OrderPaymentEditSqlQueryPermission и OrderPositionEditSqlQueryPermission:

Template.xml
<Permission Name="OrderEditPermission">
  <AccessPoint Name="OrderAddAccessPoint" />
  <AccessPoint Name="OrderEditAccessPoint" />
  <AccessPoint Name="OrderDeleteAccessPoint" />
  <SqlQuery Name="EmptyOrderInsertSqlQuery" />
  <SqlQuery Name="OrderUpdateSqlQuery" />
  <SqlQuery Name="OrderDeleteSqlQuery" />
  <SqlQuery Name="EmptyOrderDeleteSqlQuery" />
  <!-- Дополнительно -->
  <SqlQuery Name="OrderPaymentInsertSqlQuery" />
  <SqlQuery Name="OrderPaymentUpdateSqlQuery" />
  <SqlQuery Name="OrderPaymentDeleteSqlQuery" />
  <SqlQuery Name="OrderPositionInsertSqlQuery" />
  <SqlQuery Name="OrderPositionUpdateSqlQuery" />
  <SqlQuery Name="OrderPositionDeleteSqlQuery" />
</Permission>

CashViewPermission

В CashViewPermission так же добавим необходимые запросы AccountSelectSqlQuery и OperationCashSelectSqlQuery:

Template.xml
<Permission Name="CashViewPermission">
  <AccessPoint Name="CashViewAccessPoint" />
  <SqlQuery Name="CashSelectSqlQuery" />
  <SqlQuery Name="CashByIdSelectSqlQuery" />
  <!-- Дополнительно -->
  <SqlQuery Name="AccountSelectSqlQuery" />
  <SqlQuery Name="OperationCashSelectSqlQuery" />
</Permission>

ClientViewPermission

А в ClientViewPermission добавим запрос на список городов CityShortSelectSqlQuery:

Template.xml
<Permission Name="ClientViewPermission">
  <AccessPoint Name="ClientViewAccessPoint" />
  <SqlQuery Name="ClientSelectSqlQuery" />
  <SqlQuery Name="ClientByIdSelectSqlQuery" />
  <!-- Дополнительно -->
  <SqlQuery Name="CityShortSelectSqlQuery" />
</Permission>

Permission в базе данных

Помимо системных таблиц (template.permission и template.group_permission) создадим пару дополнительных таблиц, которые упростят нам работу с динамическими правами:

  • template.permission_block - будет описывать категории разрешений. Например, "Списки" или "Финансы";

  • template.permission_block_item - будет описывать группу разрешений, необходимых для чтения и редактирования списка сущности. Например, "Клиенты", "Заказы" или "Касса".

Выполним скрипт создания таблиц:

CREATE SEQUENCE template.permission_block_id_seq;
CREATE TABLE template.permission_block
(
  permission_block_id smallint NOT NULL DEFAULT nextval('template.permission_block_id_seq'::regclass),
  id_title character varying NOT NULL,
  title character varying NOT NULL,
  by_default boolean NOT NULL DEFAULT false,
  CONSTRAINT pk_permission_block_id PRIMARY KEY (permission_block_id),
  CONSTRAINT uniq_permission_block_name UNIQUE (id_title)
);

CREATE SEQUENCE template.permission_block_item_id_seq;
CREATE TABLE template.permission_block_item
(
  permission_block_item_id smallint NOT NULL DEFAULT nextval('template.permission_block_item_id_seq'::regclass),
  permission_block_id smallint NOT 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) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT uniq_permission_block_item_name UNIQUE (id_title)
);

Обратите внимание, что в таблицах устанавливаем ограничение уникальности значений в полях id_title.

Внесем корректировку в таблицу template.permission:

ALTER TABLE template.permission
  ADD COLUMN permission_block_item_id smallint NOT NULL;

Заполним таблицы данными:

INSERT INTO template.permission_block(id_title, title, by_default)
VALUES
  ('base', 'Доступы по умолчанию', true), -- 1
  ('user_action', 'Действия пользователя', false), -- 2
  ('list', 'Списки', false), -- 3
  ('main_functionality', 'Основная деятельность', false), -- 4
  ('finance', 'Финансы', false), -- 5
  ('report', 'Отчеты', false), -- 6
  ('administration', 'Администрирование', false); -- 7

INSERT INTO template.permission_block_item(permission_block_id, id_title, title)
VALUES
  -- Доступы по умолчанию
  (1, 'login', 'Вход в программу'), -- 1
  -- Действия пользователя
  (2, 'password_edit', 'Смена собственного пароля'), -- 2
  -- Списки
  (3, 'city', 'Города'), -- 3
  (3, 'client', 'Клиенты'), -- 4
  (3, 'account', 'Счета'), -- 5
  (3, 'operation', 'Назначения платежей'), -- 6
  (3, 'unit', 'Единицы измерения'), -- 7
  (3, 'material', 'ТМЦ'), -- 8
  -- Основная деятельность
  (4, 'order', 'Заказы'), -- 9
  -- Финансы
  (5, 'cash', 'Касса'), -- 10
  -- Отчеты
  (6, 'budget_report', 'Отчет по бюджету'), -- 11
  (6, 'deleted_order_report', 'Удаленные заказы'), -- `12
  -- Администрирование
  (7, 'user', 'Пользователи'), -- 13
  (7, 'group', 'Группы пользователей'), -- 14
  (7, 'settings', 'Настройки'); -- 15

  INSERT INTO template.permission(name, permission_block_item_id)
  VALUES
  -- Вход в программу
    ('BaseViewPermission', 1),
  -- Смена собственного пароля
    ('UserPasswordEditPermission', 2),
  -- Города
    ('CityViewPermission', 3),
    ('CityEditPermission', 3),
  -- Клиенты
    ('ClientViewPermission', 4),
    ('ClientEditPermission', 4),
  -- Счета
    ('AccountViewPermission', 5),
    ('AccountEditPermission', 5),
  -- Назначения платежей
    ('OperationViewPermission', 6),
    ('OperationEditPermission', 6),
  -- Единицы измерения
    ('UnitViewPermission', 7),
    ('UnitEditPermission', 7),
  -- ТМЦ
    ('MaterialCategoryViewPermission', 8),
    ('MaterialCategoryEditPermission', 8),
    ('MaterialViewPermission', 8),
    ('MaterialEditPermission', 8),
  -- Основная деятельность
    ('OrderViewPermission', 9),
    ('OrderEditPermission', 9),
  -- Финансы
    ('CashViewPermission', 10),
    ('CashEditPermission', 10),
  -- Отчет по бюджету
    ('BudgetReportPermission', 11),
  -- Удаленные заказы
    ('DeletedOrderReportPermission', 12),
  -- Пользователи
    ('UserViewPermission', 13),
    ('UserEditPermission', 13),
  -- Группы пользователей
    ('GroupViewPermission', 14),
    ('GroupEditPermission', 14),
  -- Настройки
    ('SettingsViewPermission', 15),
    ('SettingsEditPermission', 15);

Убедитесь, что вставляемые в таблицу 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>:

Template.xml
<UserSettings GroupTable="template.group"
              GroupGroupTable="template.group_group"
              UserGroupTable="template.user_group"
              Table="template.user"
              PermissionTable="template.permission"
              GroupPermissionTable="template.group_permission" />

Тем самым мы переведем серверную часть на работу с динамическими правами доступа и укажем, в каких таблицах находится необходимая информация.

Запустите приложение и проверьте загрузку главной формы для обеих групп пользователей.

Для группы "Пользователи" будут падать предупреждения, что у пользователя нет прав доступа к запросам CitySimpleSelectSqlQuery и ClientSimpleSelectSqlQuery. Чтобы исправить, вынесем их в новую команду AdditionalQueriesForOrdersDataConnectionRefreshCommand и добавим на нее условие OrderViewAccessPointCondition:

TemplateStart.xml
<Command Name="AdditionalQueriesForOrdersDataConnectionRefreshCommand" Type="DataConnectionRefreshCommand" Assembly="Commands">
  <Condition Name="OrderViewAccessPointCondition" />
  <Parallel>True</Parallel>
  <DataConnections>
    <DataConnection Name="CityPrimaryGetDataConnection" />
    <DataConnection Name="ClientPrimaryGetDataConnection" />
  </DataConnections>
</Command>

Добавим вызов новой команды во все места, где вызывается команда AllPrimaryGetDataConnectionRefreshCommand.

Список групп пользователей

Теперь можем заняться формами для редактирования списка групп пользователей. В этом разделе создадим форму списка и карточку для его редактирования, которая будет работать в двух режимах: редактирование наименования и описания для системных групп и настройка прав доступа для пользовательских групп:

Форма списка

Создайте форму для списка групп пользователей:

На главной форме (TemplateStart.xml) в меню добавьте пункт Администрирование -> Группы пользователей..., по которому будет открываться новая форма.

Скорректируем текст запроса для списка групп:

GroupSelectSqlQuery
Template.xml
 <SqlQuery Name="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>
GroupArchiveUpdateSqlQuery
Template.xml
<SqlQuery Name="GroupArchiveUpdateSqlQuery">
  <Text>
    SELECT template.group_try_archive({GroupId}::smallint, {Archive});
  </Text>
</SqlQuery>

Не забудьте сразу создать необходимые AccessPoint и добавить их на формы.

Создайте функции для удаления и работы с архивом:

template.group_try_delete(smallint)
CREATE OR REPLACE FUNCTION template.group_try_delete(in_group_id smallint)
  RETURNS character varying AS
$BODY$
BEGIN
  IF (SELECT NOT EXISTS(SELECT * FROM template.group WHERE group_id = in_group_id))
  THEN
    RETURN NULL;
  END IF;

  IF (SELECT name IS NOT NULL FROM template.group WHERE group_id = in_group_id)
  THEN
    RETURN 'system';
  END IF;

  IF (SELECT used from template.is_used('group', 'group_id', ARRAY['group_permission']::text[], ARRAY[]::text[], in_group_id))
  THEN
    UPDATE template.group
    SET archive = TRUE
    WHERE group_id = in_group_id AND NOT archive;

    RETURN 'used';
  ELSE
    DELETE FROM template.group
    WHERE group_id = in_group_id;

    RETURN NULL;
  END IF;
END;
$BODY$
  LANGUAGE plpgsql;
template.group_try_archive(smallint, boolean)
CREATE OR REPLACE FUNCTION template.group_try_archive(
    in_group_id smallint,
    in_archive boolean)
  RETURNS character varying AS
$BODY$
BEGIN
  IF (SELECT name IS NOT NULL FROM template.group WHERE group_id = in_group_id) THEN
    RETURN 'system';
  END IF;

  UPDATE template.group
  SET archive = in_archive
  WHERE group_id = in_group_id;

  RETURN NULL;
END;
$BODY$
  LANGUAGE plpgsql;

На форму списка групп пользователей (TemplateGroupList.xml) в таблицу добавьте колонки:

<Column Name="Description" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
  <Title>Описание</Title>
  <MinimumWidth>100</MinimumWidth>
  <AutoSizeMode Value="Fill" />
</Column>
<Column Name="System" Type="DatabaseTableColumnCheckBox" Assembly="DatabaseTableColumnControls">
  <Visible>False</Visible>
</Column>

Колонка System необходима для того, чтобы ограничить возможность удалять и отправлять в архив системные группы. Создайте соответствующее условие и реализуйте эти ограничения. Значения из этой колонки будем передавать в одноименный параметр на форму редактирования группы.

Добавьте проверку результата команды архивирования группы пользователей. Переделайте проверку результата команды удаления.

Карточка группы

Перейдем в файл карточки группы (TemplateGroupEdit.xml) и добавим шрифт, который пригодится в таблице прав доступа:

TemplateGroupEdit.xml
<FontStyle Name="TitleBoldFont" Font="Segoe UI" Size="9" Bold="True" />

Как говорилось ранее, карточка группы будет работать в двух режимах:

  • Полный доступ - для системных групп, которым права доступа задает разработчик. Таким группам пользователь сможет менять отображаемое имя и описание. Например, группа "Администраторы", у которой полный доступ ко всем данным и формам в приложении;

  • Настраиваемый доступ - для пользовательских групп, которым пользователь сам настраивает права доступа.

Запросы

Перейдем в файл Template.xml и скорректируем текст запроса GroupByIdSelectSqlQuery:

Template.xml
<SqlQuery Name="GroupByIdSelectSqlQuery">
  <Text>
    SELECT
      title AS "Title",
      description AS "Description"
    FROM
      template.group
    WHERE
      group_id = {GroupId};
  </Text>
</SqlQuery>

Добавим запрос на получение дерева прав доступа с пометкой выбранных прав доступа для редактируемой группы:

Template.xml
<SqlQuery Name="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>

В запросе используется функция:

CREATE OR REPLACE FUNCTION public.array_compare(
    anyarray,
    anyarray)
  RETURNS boolean AS
$BODY$
  SELECT $1 @> $2 AND $1 <@ $2;
$BODY$
  LANGUAGE sql STABLE STRICT
  COST 100;

Добавьте запрос PermissionBlockItemSelectSqlQuery в GroupViewPermission.

Форма

Добавим параметры, с помощью которых будем изменять размер формы, чтобы для пользовательских групп отображался список прав доступа:

TemplateGroupEdit.xml
<Parameter Name="System">False</Parameter>
<Parameter Name="Height">
  <Switch>
    <Case>
      <When>
        <Parameter Name="System" />
      </When>
      <Then>195</Then>
    </Case>
    <Case>750</Case>
  </Switch>
</Parameter>

Создайте на форме объекты TitleTextBox (Наименование), DescriptionTextBox (Описание) и PermissionPanel, в которой будет располагаться таблица PermissionDatabaseTable с заголовком PermissionLabel (Настраиваемые права доступа):

TemplateGroupEdit.xml
<MyObject Name="PermissionDatabaseTable" Type="DatabaseTable" Assembly="ComplexControls">
  <Top>
    <Object Name="PermissionLabel">
      <Property Name="Bottom" />
    </Object>
  </Top>
  <Left>
    <Object Name="PermissionLabel">
      <Property Name="Left" />
    </Object>
  </Left>
  <Height>
    <Formula>
      <Minus DataType="IntegerDataType">
        <Item>
          <Object Name="PermissionPanel">
            <Property Name="Height" />
          </Object>
        </Item>
        <Item>
          <Object Name="PermissionDatabaseTable">
            <Property Name="Top" />
          </Object>
        </Item>
        <Item>10</Item>
      </Minus>
    </Formula>
  </Height>
  <Width>
    <Formula>
      <Minus DataType="IntegerDataType">
        <Item>
          <Object Name="PermissionPanel">
            <Property Name="Width" />
          </Object>
        </Item>
        <Item>
          <Object Name="PermissionDatabaseTable">
            <Property Name="Left" />
          </Object>
        </Item>
        <Item>10</Item>
      </Minus>
    </Formula>
  </Width>
  <AllowResizeColumns Value="False" />
  <AllowResizeRows Value="False" />
  <TabIndex>1</TabIndex>
  <AllowInsert>False</AllowInsert>
  <AllowUpdate>True</AllowUpdate>
  <AllowDelete>False</AllowDelete>
  <AutoSizeColumnsMode Value="Fill" />
  <AllowFilterColumns Value="False" />
  <ShowCellHints Value="True" />
  <SourceDataConnection Name="PermissionBlockItemPrimaryGetDataConnection" />
  <Columns>
    <Column Name="PermissionBlockRowNumber" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
      <Visible>False</Visible>
    </Column>
    <Column Name="PermissionBlockItemRowNumber" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
      <Visible>False</Visible>
    </Column>
    <Column Name="RowNumber" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
      <Title>№</Title>
      <Width>30</Width>
      <AutoSizeMode Value="None" />
      <AllowSort Value="False" />
      <Alignment Value="MiddleCenter" />
      <Calculate>
        <Expression>PermissionBlockRowNumber + IIF(PermissionBlockItemRowNumber = 0, '', '.' + PermissionBlockItemRowNumber)</Expression>
      </Calculate>
    </Column>
    <Column Name="PermissionBlockId" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
      <Visible>False</Visible>
    </Column>
    <Column Name="PermissionBlockItemId" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
      <Visible>False</Visible>
    </Column>
    <Column Name="Title" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
      <Title>Наименование</Title>
      <Width>400</Width>
      <MinimumWidth>100</MinimumWidth>
      <ReadOnly>True</ReadOnly>
      <AllowSort Value="False" />
    </Column>
    <Column Name="Checked" Type="DatabaseTableColumnCheckBox" Assembly="DatabaseTableColumnControls">
      <Hint>Чтение/запись</Hint>
      <Width>50</Width>
      <AutoSizeMode Value="None" />
      <AllowSort Value="False" />
      <HeaderCheckAll Value="True" />
    </Column>
  </Columns>
  <Formatting>
    <FontStyle Name="TitleBoldFont">
      <Expression>PermissionBlockItemId IS NULL</Expression>
    </FontStyle>
  </Formatting>
</MyObject>

Где PermissionBlockItemPrimaryGetDataConnection имеет вид:

TemplateGroupEdit.xml
<DataConnection Name="PermissionBlockItemPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <SqlQuery Name="PermissionBlockItemSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="PermissionBlockRowNumber" />
      <Field Name="PermissionBlockItemRowNumber" />
      <Field Name="PermissionBlockId" />
      <Field Name="PermissionBlockItemId" />
      <Field Name="Title" />
      <Field Name="Checked" />
    </Fields>
    <Parameters>
      <Parameter NativeName="GroupId" RefreshQuery="False">
        <Value>
          <Parameter Name="GroupId" />
        </Value>
      </Parameter>
    </Parameters>
  </SqlQuery>
</DataConnection>

Запустите приложение и проверьте отображение объектов формы:

Так же проверьте отображение списка возможных прав доступа:

Скорректируем логику выбора прав доступа в таблице, чтобы было проще работать с таблицей. Реализуем два момента:

  • Когда ставим галочку в строке с заголовком блока (Например, по строке "Отчеты"), то галочки должны проставиться во все строки этого блока. И наоборот, если галочку снимаем с заголовка блока, то должны сняться галочки со всех строк блока;

  • Когда поставили галочки на всех строках блока, то автоматически должна проставиться галочка на заголовке блоке. Если сняли галочку хотя бы с одной строки блока, то должны снять галочку и с заголовка блока.

Нам понадобится условие проверки изменения значения ячейки в колонке (CellValueChangedCondition):

TemplateGroupEdit.xml
<Condition Name="PermissionCheckedCellValueChangedCondition" Type="CellValueChangedCondition" Assembly="Conditions">
  <Table Name="PermissionDatabaseTable" />
  <ColumnName>Checked</ColumnName>
</Condition>

Добавим условие проверки, что в выбранной строке в колонке PermissionBlockItemId пустое значение, то есть пользователь кликнул по строке заголовка блока:

TemplateGroupEdit.xml
<Condition Name="PermissionBlockItemIdIsNullCondition" Type="IsNullCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="PermissionDatabaseTable">
        <Property Name="SelectedRowCellValueByColumnName">
          <Parameters>
            <Parameter Name="ColumnName">PermissionBlockItemId</Parameter>
          </Parameters>
        </Property>
      </Object>
    </Item>
  </Items>
</Condition>

Создадим условие которое будет проверять, все ли записи из одного блока имеют одинаковое значение в колонке Checked:

TemplateGroupEdit.xml
<Condition Name="PermissionCheckedAllCondition" Type="EqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Array>
        <Source>
          <Object Name="PermissionDatabaseTable">
            <Property Name="DictionaryArrayData" />
          </Object>
        </Source>
        <Select>
          <Items>
            <Item Type="Field">PermissionBlockId</Item>
            <Item Type="Field">PermissionBlockItemId</Item>
            <Item Type="Field">Checked</Item>
          </Items>
        </Select>
        <Filter Type="Nested">
          <And>
            <Filter>
              <Index Value="0" />
              <DataType Type="IntegerDataType" />
              <Value>
                <Object Name="PermissionDatabaseTable">
                  <Property Name="SelectedRowCellValueByColumnName">
                    <Parameters>
                      <Parameter Name="ColumnName">PermissionBlockId</Parameter>
                    </Parameters>
                  </Property>
                </Object>
              </Value>
            </Filter>
            <Filter Type="IsNotNull">
              <Index Value="1" />
              <DataType Type="IntegerDataType" />
              <Value />
            </Filter>
          </And>
        </Filter>
        <Distinct>
          <On Index="2" />
        </Distinct>
        <Count />
      </Array>
    </Item>
    <Item>1</Item>
  </Items>
</Condition>

С особенностями универсального значения <Array> мы уже немного знакомы. В разделе "Дополнительно" из блока "Основной" в статье Array разбирали несколько примеров. Если Вы пропустили эту статью, советуем сначала прочитать ее, а затем вернуться к уроку и продолжить выполнение задания.

Ниже подробно рассмотрим <Array> из нового условия.

Из таблицы PermissionDatabaseTable с помощью get-проперти DictionaryArrayData получаем словарь массивов со значениями каждой колонки, который передаем в тэг <Source> в качестве источника для <Array>.

<Source>
  <Object Name="PermissionDatabaseTable">
    <Property Name="DictionaryArrayData" />
  </Object>
</Source>

Из полученного словаря нам интересны только три колонки (PermissionBlockId, PermissionBlockItemId и Checked), значения которых достаем в тэге <Select> по имени и формируем матрицу значений.

<Select>
  <Items>
    <Item Type="Field">PermissionBlockId</Item>
    <Item Type="Field">PermissionBlockItemId</Item>
    <Item Type="Field">Checked</Item>
  </Items>
</Select>

Затем фильтруем матрицу и оставляем только те строки, у которых значение 0-го элемента (колонка PermissionBlockId) совпадает со значением в колонке PermissionBlockId выделенной строки и значение 1-го элемента (колонка PermissionBlockItemId) не является пустым.

<Filter Type="Nested">
  <And>
    <Filter>
      <Index Value="0" />
      <DataType Type="IntegerDataType" />
      <Value>
        <Object Name="PermissionDatabaseTable">
          <Property Name="SelectedRowCellValueByColumnName">
            <Parameters>
              <Parameter Name="ColumnName">PermissionBlockId</Parameter>
            </Parameters>
          </Property>
        </Object>
      </Value>
    </Filter>
    <Filter Type="IsNotNull">
      <Index Value="1" />
      <DataType Type="IntegerDataType" />
      <Value />
    </Filter>
  </And>
</Filter>

Иными словами, мы получаем из таблицы строки из одного блока с выделенной строкой и исключаем строку с именем самого блока.

С помощью тэга <Distinct> мы формируем массив уникальных значений по 2-ому элементу (колонка Checked).

<Distinct>
  <On Index="2" />
</Distinct>

После с помощью тэга <Count> считаем их количество, которое передается в условие и сравнивается с единицей.

<Count />

Если все строки имеют одинаковое значение в колонке Checked, то тэг <Count> вернет значение 1, и значение условия PermissionCheckedAllCondition будет True.

Теперь создадим команду, которая будет всем строкам блока в колонку Checked проставлять значение выделенной строки:

TemplateGroupEdit.xml
<Command Name="PermissionCheckAllValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="PermissionDatabaseTable">
    <Property Name="UpdateRows">
      <Parameters>
        <Parameter Name="RowIndices">
          <Object Name="PermissionDatabaseTable">
            <Property Name="RowsIndicesOf">
              <Parameters>
                <Parameter Name="ColumnNames">
                  <Structure Type="List">
                    <Item>PermissionBlockId</Item>
                  </Structure>
                </Parameter>
                <Parameter Name="Values">
                  <Object Name="PermissionDatabaseTable">
                    <Property Name="SelectedRowCellValueByColumnName">
                      <Parameters>
                        <Parameter Name="ColumnName">PermissionBlockId</Parameter>
                      </Parameters>
                    </Property>
                  </Object>
                </Parameter>
              </Parameters>
            </Property>
          </Object>
        </Parameter>
        <Parameter Name="ColumnNames">
          <Structure Type="List">
            <Item>Checked</Item>
          </Structure>
        </Parameter>
        <Parameter Name="Values">
          <Structure Type="List">
            <Item>
              <Object Name="PermissionDatabaseTable">
                <Property Name="SelectedRowCellValueByColumnName">
                  <Parameters>
                    <Parameter Name="ColumnName">Checked</Parameter>
                  </Parameters>
                </Property>
              </Object>
            </Item>
          </Structure>
        </Parameter>
      </Parameters>
    </Property>
  </Object>
</Command>

Эту команду будем вызывать, если условие PermissionCheckedAllCondition имеет значение True.

Теперь создадим команду, которая будет снимать галочку с заголовка блока, если строки одного блока имеют разные значения в колонке Checked:

TemplateGroupEdit.xml
<Command Name="PermissionCheckValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="PermissionDatabaseTable">
    <Property Name="UpdateRow">
      <Parameters>
        <Parameter Name="RowIndex">
          <Array>
            <Source>
              <Object Name="PermissionDatabaseTable">
                <Property Name="DictionaryArrayData" />
              </Object>
            </Source>
            <Select>
              <Items>
                <Item Type="Number" />
                <Item Type="Field">PermissionBlockId</Item>
                <Item Type="Field">PermissionBlockItemId</Item>
              </Items>
            </Select>
            <Filter Type="Nested">
              <And>
                <And>
                  <Filter>
                    <Index Value="1" />
                    <DataType Type="IntegerDataType" />
                    <Value>
                      <Object Name="PermissionDatabaseTable">
                        <Property Name="SelectedRowCellValueByColumnName">
                          <Parameters>
                            <Parameter Name="ColumnName">PermissionBlockId</Parameter>
                          </Parameters>
                        </Property>
                      </Object>
                    </Value>
                  </Filter>
                  <Filter Type="IsNull">
                    <Index Value="2" />
                    <DataType Type="IntegerDataType" />
                    <Value />
                  </Filter>
                </And>
              </And>
            </Filter>
          </Array>
        </Parameter>
        <Parameter Name="ColumnNames">
          <Structure Type="List">
            <Item>Checked</Item>
          </Structure>
        </Parameter>
        <Parameter Name="Values">
          <Structure Type="List">
            <Item>False</Item>
          </Structure>
        </Parameter>
      </Parameters>
    </Property>
  </Object>
</Command>

В этой команде для параметра RowIndex с помощью <Array> находим индекс строки заголовка блока для выделенной строки.

Создадим Execution, в котором соберем все условия и команды:

TemplateGroupEdit.xml
<Execution>
  <ConditionExpression>
    <Condition Name="PermissionCheckedCellValueChangedCondition" />
  </ConditionExpression>
  <Commands>
    <If>
      <When>
        <Condition Name="PermissionBlockItemIdIsNullCondition" />
      </When>
      <Then>
        <Command Name="PermissionCheckAllValueSetCommand" />
      </Then>
      <Else>
        <If>
          <When>
            <Condition Name="PermissionCheckedAllCondition" />
          </When>
          <Then>
            <Command Name="PermissionCheckAllValueSetCommand" />
          </Then>
          <Else>
            <Command Name="PermissionCheckValueSetCommand" />
          </Else>
        </If>
      </Else>
    </If>
  </Commands>
</Execution>

Поймав событие изменения значения в колонке Checked таблицы PermissionDatabaseTable, первым делом проверяем, был ли клик по строке заголовка блока. Если выделенная строка является заголовком блока, то всем строкам блока проставим ее значение. Если выделенная строка не является заголовком блока, то проверяем значения в колонке Checked для всех строк блока. Если эти значения одинаковые, то и заголовку блока проставим такое же значение, иначе снимем галочку с заголовка блока.

Запустите приложение и проверьте работу формы TemplateGroupEdit.xml.

Сохранение прав доступа

Скорректируем сохранение изменений для прав доступа.

Чтобы извлечь из таблицы массив идентификаторов разрешений (PermissionBlockItemId), которые пользователь выбрал для группы, воспользуемся get-проперти FilteredColumnValues объекта DatabaseTable:

<Object Name="PermissionDatabaseTable">
  <Property Name="FilteredColumnValues">
    <Parameters>
      <Parameter Name="ColumnName">PermissionBlockItemId</Parameter>
      <Parameter Name="Filter">Checked AND PermissionBlockItemId IS NOT NULL</Parameter>
    </Parameters>
  </Property>
</Object>

В параметр ColumnName передаем имя колонки, из которой хотим получить значения. А в параметр Filter укажем условие выборки строк: в колонке Checked должно стоять значение true, и значение в колонке PermissionBlockItemId не должно быть пустым.

Эту конструкцию укажем в качестве значения нового параметра для GroupInsertSetDataConnection и GroupUpdateSetDataConnection:

<Parameter NativeName="PermissionBlockItemId" SendAsArray="True">
  <Value>
    <Object Name="PermissionDatabaseTable">
      <Property Name="FilteredColumnValues">
        <Parameters>
          <Parameter Name="ColumnName">PermissionBlockItemId</Parameter>
          <Parameter Name="Filter">Checked AND PermissionBlockItemId IS NOT NULL</Parameter>
        </Parameters>
      </Property>
    </Object>
  </Value>
</Parameter>

Обратите внимание, что у тэга <Parameter> появился атрибут SendAsArray со значением True, таким образом, значение параметра будет передаваться как массив, и в запросе вместо переменной будет подставляться конструкция ARRAY[..] с переданными значениями. В противном случае на сервер передавался бы первый элемент массива.

Скорректируем запросы сохранения изменений:

Template.xml
<SqlQuery Name="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
<SqlQuery Name="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-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.

Last updated