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

В прошлом уроке мы рассмотрели статические права доступа, когда разработчик через серверный xml-файл настраивает разрешения для групп пользователей. Но платформа Workflow Technology предусматривает возможность использования в приложении динамических прав доступа. Таким образом, пользователи через интерфейс программы могут настраивать разрешения для групп.&#x20;

Механизм динамических прав доступа будет отличным решением, когда пользователи могут самостоятельно редактировать список групп в программе.

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](../lesson_access_rights#answer) прошлого урока. Скопируйте папки _Forms_, _Workflow_ и _Patterns_ в папку с развернутым проектом, например, в папку _D:\WT\Projects\Template\Projects\1. Template_.

Инструкция по подключению шаблонов находится по [ссылке](../../main/lesson_list_form#podklyuchenie-shablonov-k-proektu).
{% endhint %}

## Динамические права доступа <a href="#dynamic-permissions" id="dynamic-permissions"></a>

Настройка динамических прав доступа вынесена из серверной xml в базу данных.

Для этих целей существуют таблицы **template.permission** и **template.group\_permission**. В первой мы должны перечислить имена всех Permission из серверного xml-файла, а во второй задать соответствие групп пользователей и доступных им Permission.

Описание Roles и Groups в серверном xml-файле теперь необязательно, но они не будут противоречить динамическим правам доступа. При совместном использовании статических и динамических прав доступа используется принцип разрешения. Иными словами, если в xml-файле у группы нет прав на выполнения какого-то запроса, а в таблице в базе данных такие права есть, то сервер будет разрешать пользователю выполнять этот запрос.

### Подготовка Permission <a href="#preparation-of-permission" id="preparation-of-permission"></a>

Первым делом скорректируем набор Permission в серверном xml-файле: какие-то разрешения мы объединим, чтобы уменьшить их количество, а какие-то мы дополним необходимыми запросами. Таким образом, нам будет проще в дальнейшем настроить интерфейс для динамических прав.

Перейдем в файл серверной xml (Template.xml).

#### **MaterialViewPermission** <a href="#material_view_permission" id="material_view_permission"></a>

Скорректируем MaterialViewPermission, добавив запрос на получение списка единиц измерений:

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

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

#### OrderViewPermission <a href="#order_view_permission" id="order_view_permission"></a>

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

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

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

{% code title="Template.xml" %}
```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>
```
{% endcode %}

#### OrderEditPermission <a href="#order_edit_permission" id="order_edit_permission"></a>

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

{% code title="Template.xml" %}
```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>
```
{% endcode %}

#### CashViewPermission  <a href="#cash_view_permission" id="cash_view_permission"></a>

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

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

#### ClientViewPermission  <a href="#client_view_permission" id="client_view_permission"></a>

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

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

### Permission в базе данных <a href="#permission-in-database" id="permission-in-database"></a>

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

* template.permission\_block - будет описывать категории разрешений. Например, "Списки" или "Финансы";
* template.permission\_block\_item - будет описывать группу разрешений, необходимых для чтения и редактирования списка сущности. Например, "Клиенты", "Заказы" или "Касса".

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

```sql
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)
);
```

Обратите внимание, что в таблицах устанавливаем [ограничение уникальности](https://postgrespro.ru/docs/postgrespro/16/ddl-constraints#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS) значений в полях **id\_title**.

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

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

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

```sql
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);
```

{% hint style="warning" %}
Убедитесь, что вставляемые в таблицу **template.permission** имена разрешений _совпадают_ с именами **Permission** в серверном xml-файле.
{% endhint %}

Таким образом, мы сформировали 7 основных блоков (категорий разрешений), в которых определили 15 групп, добавили имена всех разрешений из серверного файла и распределили их по группам.

В запросах мы создаем блок "Группы пользователей" и добавляем в него два разрешения **GroupViewPermission** и **GroupEditPermission**. Они пригодятся нам дальше, когда будем реализовывать возможность пользователям самим добавлять в программу новые группы пользователей.

У группы UserGroup в таблице **template.group** удалим значение из колонки **name**. Тем самым уберем группу из системных и дадим возможность настраивать ей права доступа через интерфейс программы. Можем для группы "Пользователи" изменить описание на "Настраиваемый доступ". Не забудьте удалить с колонки **name** ограничение **NOT NULL**.

Добавим группе "Администраторы" (AdministratorGroup) все права доступа, а группам "Пользователи" и "Гости" (GuestGroup) только общие права доступа - **BaseViewPermission**.

Вернемся в серверный xml-файл (Template.xml) и удалим тэги `<Roles>` и `<Groups>` - теперь будем работать только через динамические права доступа, чтобы не возникало путаницы с разрешениями прав доступа.

Добавим в Template.xml тэг `<UserSettings>` сразу после открывающего тэга `<Workflow>`:

{% code title="Template.xml" %}
```xml
<UserSettings GroupTable="template.group"
              GroupGroupTable="template.group_group"
              UserGroupTable="template.user_group"
              Table="template.user"
              PermissionTable="template.permission"
              GroupPermissionTable="template.group_permission" />
```
{% endcode %}

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

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

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

{% code title="TemplateStart.xml" %}
```xml
<Command Name="AdditionalQueriesForOrdersDataConnectionRefreshCommand" Type="DataConnectionRefreshCommand" Assembly="Commands">
  <Condition Name="OrderViewAccessPointCondition" />
  <Parallel>True</Parallel>
  <DataConnections>
    <DataConnection Name="CityPrimaryGetDataConnection" />
    <DataConnection Name="ClientPrimaryGetDataConnection" />
  </DataConnections>
</Command>
```
{% endcode %}

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

## Список групп пользователей <a href="#list-of-user-groups" id="list-of-user-groups"></a>

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

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FGXnj0TMKOBmMGapd8zaf%2F01.png?alt=media\&token=e4cc12f6-0f02-4c4a-9f4c-60b73a4fca66)

### Форма списка <a href="#list-form" id="list-form"></a>

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

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2F1IQJeRHPG3cQTr3oOvwq%2Fimage.png?alt=media&#x26;token=5af5f56d-9e49-4797-be51-6a67f9a20f85" alt=""><figcaption></figcaption></figure>

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

![](https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FYgmpvuizquabAJ1pPw7o%2F02.png?alt=media\&token=20d2ecf9-9d88-4aac-8f6d-9781324c9ddc)

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

<details>

<summary>GroupSelectSqlQuery</summary>

{% code title="Template.xml" %}
```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>
```
{% endcode %}

</details>

<details>

<summary>GroupArchiveUpdateSqlQuery</summary>

{% code title="Template.xml" %}
```xml
<SqlQuery Name="GroupArchiveUpdateSqlQuery">
  <Text>
    SELECT template.group_try_archive({GroupId}::smallint, {Archive});
  </Text>
</SqlQuery>
```
{% endcode %}

</details>

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

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

<details>

<summary>template.group_try_delete(smallint)</summary>

```sql
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;
```

</details>

<details>

<summary>template.group_try_archive(smallint, boolean)</summary>

```sql
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;
```

</details>

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

```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** необходима для того, чтобы ограничить возможность удалять и отправлять в архив системные группы. Создайте соответствующее условие и реализуйте эти ограничения. Значения из этой колонки будем передавать в одноименный параметр на форму редактирования группы.

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

### Карточка группы <a href="#group-form" id="group-form"></a>

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

{% code title="TemplateGroupEdit.xml" %}
```xml
<FontStyle Name="TitleBoldFont" Font="Segoe UI" Size="9" Bold="True" />
```
{% endcode %}

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

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

#### Запросы <a href="#queries" id="queries"></a>

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

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

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

{% code title="Template.xml" %}
```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>
```
{% endcode %}

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

```sql
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.

#### Форма <a href="#form" id="form"></a>

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

{% code title="TemplateGroupEdit.xml" %}
```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>
```
{% endcode %}

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

{% code title="TemplateGroupEdit.xml" %}
```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>
```
{% endcode %}

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

{% code title="TemplateGroupEdit.xml" %}
```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>
```
{% endcode %}

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

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FSUtpmJvza5XPLsIKh1HD%2Fimage.png?alt=media&#x26;token=f10663d7-0a53-4465-924c-4cfaf23d37a3" alt=""><figcaption></figcaption></figure>

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

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FHWm6rK1OA7QgeEUtHdlP%2Fimage.png?alt=media&#x26;token=40a8da84-4509-4a33-8584-65b2c696737a" alt=""><figcaption></figcaption></figure>

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

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

Нам понадобится условие проверки изменения значения ячейки в колонке ([CellValueChangedCondition](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/conditions/event_condition/cell_value_changed_condition)):

{% code title="TemplateGroupEdit.xml" %}
```xml
<Condition Name="PermissionCheckedCellValueChangedCondition" Type="CellValueChangedCondition" Assembly="Conditions">
  <Table Name="PermissionDatabaseTable" />
  <ColumnName>Checked</ColumnName>
</Condition>
```
{% endcode %}

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

{% code title="TemplateGroupEdit.xml" %}
```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>
```
{% endcode %}

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

{% code title="TemplateGroupEdit.xml" %}
```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>
```
{% endcode %}

С особенностями универсального значения [`<Array>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/values/array) мы уже немного знакомы. В разделе "Дополнительно" из блока "Основной" в статье [Array](../main/additionally/array) разбирали несколько примеров. Если Вы пропустили эту статью, советуем сначала прочитать ее, а затем вернуться к уроку и продолжить выполнение задания.

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

Из таблицы **PermissionDatabaseTable** с помощью get-проперти [DictionaryArrayData](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#get_dictionary_array_data) получаем словарь массивов со значениями каждой колонки, который передаем в тэг `<Source>` в качестве источника для `<Array>`.

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

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

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

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

```xml
<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).

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

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

```xml
<Count />
```

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

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

{% code title="TemplateGroupEdit.xml" %}
```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>
```
{% endcode %}

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

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

{% code title="TemplateGroupEdit.xml" %}
```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>
```
{% endcode %}

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

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

{% code title="TemplateGroupEdit.xml" %}
```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>
```
{% endcode %}

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



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

#### Сохранение прав доступа <a href="#saving-access-rights" id="saving-access-rights"></a>

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

Чтобы извлечь из таблицы массив идентификаторов разрешений (PermissionBlockItemId), которые пользователь выбрал для группы, воспользуемся get-проперти [FilteredColumnValues](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#get_filtered_column_values) объекта DatabaseTable:

```xml
<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:

```xml
<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[..]` с переданными значениями. В противном случае на сервер передавался бы первый элемент массива.

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

{% code title="Template.xml" %}
```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>
```
{% endcode %}

Для всех новых групп сразу добавляем разрешение по умолчанию (by\_default).

{% code title="Template.xml" %}
```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>
```
{% endcode %}

Так как на таблице **template.group\_permission** стоит [ограничение уникальности](https://postgrespro.ru/docs/postgrespro/16/ddl-constraints#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS) пары group\_id и permission\_id, то в INSERT-запрос добавлена инструкция `ON CONFLICT (group_id, permission_id) DO NOTHING`. Таким образом, при нарушении ограничения уникальности PostgreSQL не будет предпринимать никаких действий.

{% hint style="info" %}
Подробнее про предложение `ON CONFLICT` можно почитать в официальном справочнике PostgreSQL по [ссылке](https://postgrespro.ru/docs/postgrespro/16/sql-insert#SQL-ON-CONFLICT).
{% endhint %}

Запустите приложение и настройте права доступа для группы "Пользователи". Создайте новую группу, чтобы проверить работу запроса на вставку данных.

<figure><img src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FxqhNGkLmDJgYpe2QPVtV%2Fimage.png?alt=media&#x26;token=89b65814-8680-473d-9095-ddd9492ef0d5" alt=""><figcaption></figcaption></figure>

## Самостоятельно <a href="#self-work" id="self-work"></a>

Все Permission на работу с заказами мы объединили в одно разрешение "Заказы". Но можно разделить их на два: одно разрешение будет на создание и редактирование заказов, а второе на добавление и редактирование оплат в заказе. С помощью такого разделения можно гибко настроить права доступа к заказам. Например, группа "Пользователи" может создавать заказы и добавлять в них оплаты, а группа "Бухгалтеры" может только просматривать карточку заказа (без редактирования) и редактировать оплаты в заказе.

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

{% hint style="info" %}
В ответах к уроку нет решения данной задачи, так как она не влияет на работу приложения в рамках обучающего курса. Но решение ее позволит Вам лучше понять разделение прав доступа между группами пользователей.
{% endhint %}

## Итоги <a href="#results" id="results"></a>

В этом уроке мы рассмотрели правила организации динамических прав доступа и реализовали их в нашем проекте.

## Ответы <a href="#answer" id="answer"></a>

В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.

{% file src="https://3019442075-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_eBlWEU4C3o2GVEAAr%2Fuploads%2FRw7htZt5MhZijKQVLfpL%2Flesson19-answer.zip?alt=media&token=9f86ed6f-1988-46b0-a1e2-2b8bbd965b6c" %}
